github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/cd-service/pkg/service/batch.go (about)

     1  /*This file is part of kuberpult.
     2  
     3  Kuberpult is free software: you can redistribute it and/or modify
     4  it under the terms of the Expat(MIT) License as published by
     5  the Free Software Foundation.
     6  
     7  Kuberpult is distributed in the hope that it will be useful,
     8  but WITHOUT ANY WARRANTY; without even the implied warranty of
     9  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    10  MIT License for more details.
    11  
    12  You should have received a copy of the MIT License
    13  along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>.
    14  
    15  Copyright 2023 freiheit.com*/
    16  
    17  package service
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  
    24  	"github.com/freiheit-com/kuberpult/pkg/grpc"
    25  	"github.com/freiheit-com/kuberpult/pkg/valid"
    26  
    27  	api "github.com/freiheit-com/kuberpult/pkg/api/v1"
    28  	"github.com/freiheit-com/kuberpult/pkg/auth"
    29  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/config"
    30  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/repository"
    31  	"google.golang.org/grpc/codes"
    32  	"google.golang.org/grpc/status"
    33  )
    34  
    35  type BatchServerConfig struct {
    36  	WriteCommitData bool
    37  }
    38  
    39  type BatchServer struct {
    40  	Repository repository.Repository
    41  	RBACConfig auth.RBACConfig
    42  	Config     BatchServerConfig
    43  }
    44  
    45  // see maxBatchActions in store.tsx
    46  const maxBatchActions int = 100
    47  
    48  func ValidateEnvironmentLock(
    49  	actionType string, // "create" | "delete"
    50  	env string,
    51  	id string,
    52  ) error {
    53  	if !valid.EnvironmentName(env) {
    54  		return status.Error(codes.InvalidArgument, fmt.Sprintf("cannot %s environment lock: invalid environment: '%s'", actionType, env))
    55  	}
    56  	if !valid.LockId(id) {
    57  		return status.Error(codes.InvalidArgument, fmt.Sprintf("cannot %s environment lock: invalid lock id: '%s'", actionType, id))
    58  	}
    59  	return nil
    60  }
    61  
    62  func ValidateEnvironmentApplicationLock(
    63  	actionType string, // "create" | "delete"
    64  	env string,
    65  	app string,
    66  	id string,
    67  ) error {
    68  	if !valid.EnvironmentName(env) {
    69  		return status.Error(codes.InvalidArgument, fmt.Sprintf("cannot %s environment application lock: invalid environment: '%s'", actionType, env))
    70  	}
    71  	if !valid.ApplicationName(app) {
    72  		return status.Error(codes.InvalidArgument, fmt.Sprintf("cannot %s environment application lock: invalid application: '%s'", actionType, app))
    73  	}
    74  	if !valid.LockId(id) {
    75  		return status.Error(codes.InvalidArgument, fmt.Sprintf("cannot %s environment application lock: invalid lock id: '%s'", actionType, id))
    76  	}
    77  	return nil
    78  }
    79  
    80  func ValidateDeployment(
    81  	env string,
    82  	app string,
    83  ) error {
    84  	if !valid.EnvironmentName(env) {
    85  		return status.Error(codes.InvalidArgument, fmt.Sprintf("cannot deploy environment application lock: invalid environment: '%s'", env))
    86  	}
    87  	if !valid.ApplicationName(app) {
    88  		return status.Error(codes.InvalidArgument, fmt.Sprintf("cannot deploy environment application lock: invalid application: '%s'", app))
    89  	}
    90  	return nil
    91  }
    92  
    93  func ValidateApplication(
    94  	app string,
    95  ) error {
    96  	if !valid.ApplicationName(app) {
    97  		return status.Error(codes.InvalidArgument, fmt.Sprintf("cannot create undeploy version: invalid application: '%s'", app))
    98  	}
    99  	return nil
   100  }
   101  
   102  func (d *BatchServer) processAction(
   103  	batchAction *api.BatchAction,
   104  ) (repository.Transformer, *api.BatchResult, error) {
   105  	switch action := batchAction.Action.(type) {
   106  	case *api.BatchAction_CreateEnvironmentLock:
   107  		act := action.CreateEnvironmentLock
   108  		if err := ValidateEnvironmentLock("create", act.Environment, act.LockId); err != nil {
   109  			return nil, nil, err
   110  		}
   111  		return &repository.CreateEnvironmentLock{
   112  			Environment:    act.Environment,
   113  			LockId:         act.LockId,
   114  			Message:        act.Message,
   115  			Authentication: repository.Authentication{RBACConfig: d.RBACConfig},
   116  		}, nil, nil
   117  	case *api.BatchAction_DeleteEnvironmentLock:
   118  		act := action.DeleteEnvironmentLock
   119  		if err := ValidateEnvironmentLock("delete", act.Environment, act.LockId); err != nil {
   120  			return nil, nil, err
   121  		}
   122  		return &repository.DeleteEnvironmentLock{
   123  			Environment:    act.Environment,
   124  			LockId:         act.LockId,
   125  			Authentication: repository.Authentication{RBACConfig: d.RBACConfig},
   126  		}, nil, nil
   127  	case *api.BatchAction_CreateEnvironmentApplicationLock:
   128  		act := action.CreateEnvironmentApplicationLock
   129  		if err := ValidateEnvironmentApplicationLock("create", act.Environment, act.Application, act.LockId); err != nil {
   130  			return nil, nil, err
   131  		}
   132  		return &repository.CreateEnvironmentApplicationLock{
   133  			Environment:    act.Environment,
   134  			Application:    act.Application,
   135  			LockId:         act.LockId,
   136  			Message:        act.Message,
   137  			Authentication: repository.Authentication{RBACConfig: d.RBACConfig},
   138  		}, nil, nil
   139  	case *api.BatchAction_DeleteEnvironmentApplicationLock:
   140  		act := action.DeleteEnvironmentApplicationLock
   141  		if err := ValidateEnvironmentApplicationLock("delete", act.Environment, act.Application, act.LockId); err != nil {
   142  			return nil, nil, err
   143  		}
   144  		return &repository.DeleteEnvironmentApplicationLock{
   145  			Environment:    act.Environment,
   146  			Application:    act.Application,
   147  			LockId:         act.LockId,
   148  			Authentication: repository.Authentication{RBACConfig: d.RBACConfig},
   149  		}, nil, nil
   150  	case *api.BatchAction_PrepareUndeploy:
   151  		act := action.PrepareUndeploy
   152  		if err := ValidateApplication(act.Application); err != nil {
   153  			return nil, nil, err
   154  		}
   155  		return &repository.CreateUndeployApplicationVersion{
   156  			Application:     act.Application,
   157  			Authentication:  repository.Authentication{RBACConfig: d.RBACConfig},
   158  			WriteCommitData: d.Config.WriteCommitData,
   159  		}, nil, nil
   160  	case *api.BatchAction_Undeploy:
   161  		act := action.Undeploy
   162  		if err := ValidateApplication(act.Application); err != nil {
   163  			return nil, nil, err
   164  		}
   165  		return &repository.UndeployApplication{
   166  			Application:    act.Application,
   167  			Authentication: repository.Authentication{RBACConfig: d.RBACConfig},
   168  		}, nil, nil
   169  	case *api.BatchAction_Deploy:
   170  		act := action.Deploy
   171  		if err := ValidateDeployment(act.Environment, act.Application); err != nil {
   172  			return nil, nil, err
   173  		}
   174  		b := act.LockBehavior
   175  		if act.IgnoreAllLocks { //nolint: staticcheck
   176  			// the UI currently sets this to true,
   177  			// in that case, we still want to ignore locks (for emergency deployments)
   178  			b = api.LockBehavior_IGNORE
   179  		}
   180  		return &repository.DeployApplicationVersion{
   181  			SourceTrain:     nil,
   182  			Environment:     act.Environment,
   183  			Application:     act.Application,
   184  			Version:         act.Version,
   185  			LockBehaviour:   b,
   186  			WriteCommitData: d.Config.WriteCommitData,
   187  			Authentication:  repository.Authentication{RBACConfig: d.RBACConfig},
   188  		}, nil, nil
   189  	case *api.BatchAction_DeleteEnvFromApp:
   190  		act := action.DeleteEnvFromApp
   191  		return &repository.DeleteEnvFromApp{
   192  			Environment:    act.Environment,
   193  			Application:    act.Application,
   194  			Authentication: repository.Authentication{RBACConfig: d.RBACConfig},
   195  		}, nil, nil
   196  	case *api.BatchAction_ReleaseTrain:
   197  		in := action.ReleaseTrain
   198  		if !valid.EnvironmentName(in.Target) {
   199  			return nil, nil, status.Error(codes.InvalidArgument, "invalid environment")
   200  		}
   201  		if in.Team != "" && !valid.TeamName(in.Team) {
   202  			return nil, nil, status.Error(codes.InvalidArgument, "invalid Team name")
   203  		}
   204  		return &repository.ReleaseTrain{
   205  				Repo:            d.Repository,
   206  				Target:          in.Target,
   207  				Team:            in.Team,
   208  				CommitHash:      in.CommitHash,
   209  				WriteCommitData: d.Config.WriteCommitData,
   210  				Authentication:  repository.Authentication{RBACConfig: d.RBACConfig},
   211  			}, &api.BatchResult{
   212  				Result: &api.BatchResult_ReleaseTrain{
   213  					ReleaseTrain: &api.ReleaseTrainResponse{Target: in.Target, Team: in.Team},
   214  				},
   215  			}, nil
   216  	case *api.BatchAction_CreateRelease:
   217  		in := action.CreateRelease
   218  		response := api.CreateReleaseResponseSuccess{}
   219  		return &repository.CreateApplicationVersion{
   220  				Version:         in.Version,
   221  				Application:     in.Application,
   222  				Manifests:       in.Manifests,
   223  				SourceCommitId:  in.SourceCommitId,
   224  				SourceAuthor:    in.SourceAuthor,
   225  				SourceMessage:   in.SourceMessage,
   226  				SourceRepoUrl:   in.SourceRepoUrl,
   227  				PreviousCommit:  in.PreviousCommitId,
   228  				NextCommit:      in.NextCommitId,
   229  				Team:            in.Team,
   230  				DisplayVersion:  in.DisplayVersion,
   231  				Authentication:  repository.Authentication{RBACConfig: d.RBACConfig},
   232  				WriteCommitData: d.Config.WriteCommitData,
   233  			}, &api.BatchResult{
   234  				Result: &api.BatchResult_CreateReleaseResponse{
   235  					CreateReleaseResponse: &api.CreateReleaseResponse{
   236  						Response: &api.CreateReleaseResponse_Success{
   237  							Success: &response,
   238  						},
   239  					},
   240  				},
   241  			}, nil
   242  	case *api.BatchAction_CreateEnvironment:
   243  		in := action.CreateEnvironment
   244  		conf := in.Config
   245  		if conf == nil {
   246  			//exhaustruct:ignore
   247  			conf = &api.EnvironmentConfig{}
   248  		}
   249  		var argocd *config.EnvironmentConfigArgoCd
   250  		if conf.Argocd != nil {
   251  			syncWindows := transformSyncWindowsToConfig(conf.Argocd.SyncWindows)
   252  			clusterResourceWhitelist := transformAccessListToConfig(conf.Argocd.AccessList)
   253  			ignoreDifferences := transformIgnoreDifferencesToConfig(conf.Argocd.IgnoreDifferences)
   254  			argocd = &config.EnvironmentConfigArgoCd{
   255  				Destination:              transformDestinationToConfig(conf.Argocd.Destination),
   256  				SyncWindows:              syncWindows,
   257  				ClusterResourceWhitelist: clusterResourceWhitelist,
   258  				ApplicationAnnotations:   conf.Argocd.ApplicationAnnotations,
   259  				IgnoreDifferences:        ignoreDifferences,
   260  				SyncOptions:              conf.Argocd.SyncOptions,
   261  			}
   262  		}
   263  		upstream := transformUpstreamToConfig(conf.Upstream)
   264  		transformer := &repository.CreateEnvironment{
   265  			Environment: in.Environment,
   266  			Config: config.EnvironmentConfig{
   267  				Upstream:         upstream,
   268  				ArgoCd:           argocd,
   269  				EnvironmentGroup: conf.EnvironmentGroup,
   270  			},
   271  			Authentication: repository.Authentication{RBACConfig: d.RBACConfig},
   272  		}
   273  		return transformer, nil, nil
   274  	case *api.BatchAction_CreateEnvironmentGroupLock:
   275  		act := action.CreateEnvironmentGroupLock
   276  		return &repository.CreateEnvironmentGroupLock{
   277  			EnvironmentGroup: act.EnvironmentGroup,
   278  			LockId:           act.LockId,
   279  			Message:          act.Message,
   280  			Authentication:   repository.Authentication{RBACConfig: d.RBACConfig},
   281  		}, nil, nil
   282  	case *api.BatchAction_DeleteEnvironmentGroupLock:
   283  		act := action.DeleteEnvironmentGroupLock
   284  		return &repository.DeleteEnvironmentGroupLock{
   285  			EnvironmentGroup: act.EnvironmentGroup,
   286  			LockId:           act.LockId,
   287  			Authentication:   repository.Authentication{RBACConfig: d.RBACConfig},
   288  		}, nil, nil
   289  	}
   290  	return nil, nil, status.Error(codes.InvalidArgument, "processAction: cannot process action: invalid action type")
   291  }
   292  
   293  func (d *BatchServer) ProcessBatch(
   294  	ctx context.Context,
   295  	in *api.BatchRequest,
   296  ) (*api.BatchResponse, error) {
   297  	user, err := auth.ReadUserFromContext(ctx)
   298  	if err != nil {
   299  		return nil, grpc.AuthError(ctx, fmt.Errorf("batch requires user to be provided %v", err))
   300  	}
   301  	ctx = auth.WriteUserToContext(ctx, *user)
   302  	if len(in.GetActions()) > maxBatchActions {
   303  		return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("cannot process batch: too many actions. limit is %d", maxBatchActions))
   304  	}
   305  
   306  	results := make([]*api.BatchResult, 0, len(in.GetActions()))
   307  	transformers := make([]repository.Transformer, 0, maxBatchActions)
   308  	for _, batchAction := range in.GetActions() {
   309  		transformer, result, err := d.processAction(batchAction)
   310  		if err != nil {
   311  			// Validation error
   312  			return nil, err
   313  		}
   314  		transformers = append(transformers, transformer)
   315  		results = append(results, result)
   316  	}
   317  	err = d.Repository.Apply(ctx, transformers...)
   318  	if err != nil {
   319  		var applyErr *repository.TransformerBatchApplyError
   320  		if errors.Is(err, repository.ErrQueueFull) {
   321  			return nil, status.Error(codes.ResourceExhausted, fmt.Sprintf("Could not process ProcessBatch request. Err: %s", err.Error()))
   322  		}
   323  
   324  		if !errors.As(err, &applyErr) {
   325  			return nil, err
   326  		}
   327  
   328  		switch transformerError := applyErr.TransformerError.(type) {
   329  		case *repository.CreateReleaseError:
   330  			{
   331  				errorResults := make([]*api.BatchResult, 1)
   332  				errorResults[0] = &api.BatchResult{
   333  					Result: &api.BatchResult_CreateReleaseResponse{
   334  						CreateReleaseResponse: transformerError.Response(),
   335  					},
   336  				}
   337  				return &api.BatchResponse{Results: errorResults}, nil
   338  			}
   339  		default:
   340  			return nil, err
   341  		}
   342  	}
   343  	return &api.BatchResponse{Results: results}, nil
   344  }
   345  
   346  var _ api.BatchServiceServer = (*BatchServer)(nil)