diff --git a/pkg/app/pipedv1/cmd/piped/piped.go b/pkg/app/pipedv1/cmd/piped/piped.go index 3270095681..b9b1d66470 100644 --- a/pkg/app/pipedv1/cmd/piped/piped.go +++ b/pkg/app/pipedv1/cmd/piped/piped.go @@ -71,6 +71,7 @@ import ( "github.com/pipe-cd/pipecd/pkg/lifecycle" "github.com/pipe-cd/pipecd/pkg/model" pluginapi "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1" + pluginRegistry "github.com/pipe-cd/pipecd/pkg/plugin/registry" "github.com/pipe-cd/pipecd/pkg/rpc" "github.com/pipe-cd/pipecd/pkg/rpc/rpcauth" "github.com/pipe-cd/pipecd/pkg/rpc/rpcclient" @@ -348,7 +349,7 @@ func (p *piped) run(ctx context.Context, input cli.Input) (runErr error) { } // Make grpc clients to connect to plugins. - pluginClis := make([]pluginapi.PluginClient, 0, len(cfg.Plugins)) + plugins := make([]pluginRegistry.Plugin, 0, len(cfg.Plugins)) options := []rpcclient.DialOption{ rpcclient.WithBlock(), rpcclient.WithInsecure(), @@ -357,8 +358,19 @@ func (p *piped) run(ctx context.Context, input cli.Input) (runErr error) { cli, err := pluginapi.NewClient(ctx, net.JoinHostPort("localhost", strconv.Itoa(plg.Port)), options...) if err != nil { input.Logger.Error("failed to create client to connect plugin", zap.String("plugin", plg.Name), zap.Error(err)) + return err } - pluginClis = append(pluginClis, cli) + + plugins = append(plugins, pluginRegistry.Plugin{ + Name: plg.Name, + Cli: cli, + }) + } + + pluginRegistry, err := pluginRegistry.NewPluginRegistry(ctx, plugins) + if err != nil { + input.Logger.Error("failed to create plugin registry", zap.Error(err)) + return err } // Start running application live state reporter. @@ -383,7 +395,7 @@ func (p *piped) run(ctx context.Context, input cli.Input) (runErr error) { c := controller.NewController( apiClient, gitClient, - pluginClis, + pluginRegistry, deploymentLister, commandLister, notifier, diff --git a/pkg/app/pipedv1/controller/controller.go b/pkg/app/pipedv1/controller/controller.go index 51f1ce7634..bfcaa836b3 100644 --- a/pkg/app/pipedv1/controller/controller.go +++ b/pkg/app/pipedv1/controller/controller.go @@ -38,7 +38,7 @@ import ( "github.com/pipe-cd/pipecd/pkg/git" "github.com/pipe-cd/pipecd/pkg/model" pluginapi "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1" - "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1/deployment" + "github.com/pipe-cd/pipecd/pkg/plugin/registry" ) type apiClient interface { @@ -96,8 +96,9 @@ type controller struct { notifier notifier secretDecrypter secretDecrypter - // gRPC clients to communicate with plugins. - pluginClients []pluginapi.PluginClient + // The registry of all plugins. + pluginRegistry registry.PluginRegistry + // Map from stage name to the plugin client. stageBasedPluginsMap map[string]pluginapi.PluginClient // Map from application ID to the planner @@ -131,7 +132,7 @@ type controller struct { func NewController( apiClient apiClient, gitClient gitClient, - pluginClients []pluginapi.PluginClient, + pluginRegistry registry.PluginRegistry, deploymentLister deploymentLister, commandLister commandLister, notifier notifier, @@ -144,7 +145,7 @@ func NewController( return &controller{ apiClient: apiClient, gitClient: gitClient, - pluginClients: pluginClients, + pluginRegistry: pluginRegistry, deploymentLister: deploymentLister, commandLister: commandLister, notifier: notifier, @@ -179,23 +180,6 @@ func (c *controller) Run(ctx context.Context) error { c.workspaceDir = dir c.logger.Info(fmt.Sprintf("workspace directory was configured to %s", c.workspaceDir)) - // Build the list of stages that can be handled by piped's plugins. - stagesBasedPluginsMap := make(map[string]pluginapi.PluginClient) - for _, plugin := range c.pluginClients { - resp, err := plugin.FetchDefinedStages(ctx, &deployment.FetchDefinedStagesRequest{}) - if err != nil { - return err - } - for _, stage := range resp.GetStages() { - if _, ok := stagesBasedPluginsMap[stage]; ok { - c.logger.Error("duplicated stage name", zap.String("stage", stage)) - return fmt.Errorf("duplicated stage name %s", stage) - } - stagesBasedPluginsMap[stage] = plugin - } - } - c.stageBasedPluginsMap = stagesBasedPluginsMap - ticker := time.NewTicker(c.syncInternal) defer ticker.Stop() c.logger.Info("start syncing planners and schedulers") @@ -448,8 +432,7 @@ func (c *controller) startNewPlanner(ctx context.Context, d *model.Deployment) ( commitHash, configFilename, workingDir, - c.pluginClients, // FIXME: Find a way to ensure the plugins only related to deployment. - c.stageBasedPluginsMap, + c.pluginRegistry, c.apiClient, c.gitClient, c.notifier, @@ -590,7 +573,7 @@ func (c *controller) startNewScheduler(ctx context.Context, d *model.Deployment) workingDir, c.apiClient, c.gitClient, - c.stageBasedPluginsMap, + c.pluginRegistry, c.notifier, c.secretDecrypter, c.logger, diff --git a/pkg/app/pipedv1/controller/planner.go b/pkg/app/pipedv1/controller/planner.go index ce5140f88f..c77ed8739a 100644 --- a/pkg/app/pipedv1/controller/planner.go +++ b/pkg/app/pipedv1/controller/planner.go @@ -36,6 +36,7 @@ import ( "github.com/pipe-cd/pipecd/pkg/model" pluginapi "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1" "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1/deployment" + "github.com/pipe-cd/pipecd/pkg/plugin/registry" "github.com/pipe-cd/pipecd/pkg/regexpool" ) @@ -57,13 +58,6 @@ type planner struct { lastSuccessfulConfigFilename string workingDir string - // The plugin clients are used to call plugin that actually - // performs planning deployment. - plugins []pluginapi.PluginClient - // The map used to know which plugin is incharged for a given stage - // of the current deployment. - stageBasedPluginsMap map[string]pluginapi.PluginClient - // The apiClient is used to report the deployment status. apiClient apiClient @@ -79,6 +73,9 @@ type planner struct { // which encrypted using PipeCD built-in secret management. secretDecrypter secretDecrypter + // The pluginRegistry is used to determine which plugins to be used + pluginRegistry registry.PluginRegistry + logger *zap.Logger tracer trace.Tracer @@ -96,8 +93,7 @@ func newPlanner( lastSuccessfulCommitHash string, lastSuccessfulConfigFilename string, workingDir string, - pluginClients []pluginapi.PluginClient, - stageBasedPluginsMap map[string]pluginapi.PluginClient, + pluginRegistry registry.PluginRegistry, apiClient apiClient, gitClient gitClient, notifier notifier, @@ -119,8 +115,7 @@ func newPlanner( lastSuccessfulCommitHash: lastSuccessfulCommitHash, lastSuccessfulConfigFilename: lastSuccessfulConfigFilename, workingDir: workingDir, - stageBasedPluginsMap: stageBasedPluginsMap, - plugins: pluginClients, + pluginRegistry: pluginRegistry, apiClient: apiClient, gitClient: gitClient, metadataStore: metadatastore.NewMetadataStore(apiClient, d), @@ -266,8 +261,21 @@ func (p *planner) buildPlan(ctx context.Context, runningDS, targetDS *deployment TargetDeploymentSource: targetDS, } + cfg, err := config.DecodeYAML[*config.GenericApplicationSpec](targetDS.GetApplicationConfig()) + if err != nil { + p.logger.Error("unable to parse application config", zap.Error(err)) + return nil, err + } + spec := cfg.Spec + + plugins, err := p.pluginRegistry.GetPluginClientsByAppConfig(spec) + if err != nil { + p.logger.Error("unable to get plugin clients by app config", zap.Error(err)) + return nil, err + } + // Build deployment target versions. - for _, plg := range p.plugins { + for _, plg := range plugins { vRes, err := plg.DetermineVersions(ctx, &deployment.DetermineVersionsRequest{Input: input}) if err != nil { p.logger.Warn("unable to determine versions", zap.Error(err)) @@ -284,13 +292,6 @@ func (p *planner) buildPlan(ctx context.Context, runningDS, targetDS *deployment } } - cfg, err := config.DecodeYAML[*config.GenericApplicationSpec](targetDS.GetApplicationConfig()) - if err != nil { - p.logger.Error("unable to parse application config", zap.Error(err)) - return nil, err - } - spec := cfg.Spec - // In case the strategy has been decided by trigger. // For example: user triggered the deployment via web console. switch p.deployment.Trigger.SyncStrategy { @@ -375,7 +376,7 @@ func (p *planner) buildPlan(ctx context.Context, runningDS, targetDS *deployment summary string ) // Build plan based on plugins determined strategy - for _, plg := range p.plugins { + for _, plg := range plugins { res, err := plg.DetermineStrategy(ctx, &deployment.DetermineStrategyRequest{Input: input}) if err != nil { p.logger.Warn("Unable to determine strategy using current plugin", zap.Error(err)) @@ -421,7 +422,12 @@ func (p *planner) buildQuickSyncStages(ctx context.Context, cfg *config.GenericA rollbackStages = []*model.PipelineStage{} rollback = *cfg.Planner.AutoRollback ) - for _, plg := range p.plugins { + + plugins, err := p.pluginRegistry.GetPluginClientsByAppConfig(cfg) + if err != nil { + return nil, err + } + for _, plg := range plugins { res, err := plg.BuildQuickSyncStages(ctx, &deployment.BuildQuickSyncStagesRequest{Rollback: rollback}) if err != nil { return nil, fmt.Errorf("failed to build quick sync stage deployment (%w)", err) @@ -462,9 +468,9 @@ func (p *planner) buildPipelineSyncStages(ctx context.Context, cfg *config.Gener // Build stages config for each plugin. for i := range stagesCfg { stageCfg := stagesCfg[i] - plg, ok := p.stageBasedPluginsMap[stageCfg.Name.String()] - if !ok { - return nil, fmt.Errorf("unable to find plugin for stage %q", stageCfg.Name.String()) + plg, err := p.pluginRegistry.GetPluginClientByStageName(stageCfg.Name.String()) + if err != nil { + return nil, err } stagesCfgPerPlugin[plg] = append(stagesCfgPerPlugin[plg], &deployment.BuildPipelineSyncStagesRequest_StageConfig{ diff --git a/pkg/app/pipedv1/controller/planner_test.go b/pkg/app/pipedv1/controller/planner_test.go index 9fb2e97eb6..497b340cb9 100644 --- a/pkg/app/pipedv1/controller/planner_test.go +++ b/pkg/app/pipedv1/controller/planner_test.go @@ -29,6 +29,7 @@ import ( "github.com/pipe-cd/pipecd/pkg/model" pluginapi "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1" "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1/deployment" + "github.com/pipe-cd/pipecd/pkg/plugin/registry" ) type fakePlugin struct { @@ -37,6 +38,7 @@ type fakePlugin struct { quickStages []*model.PipelineStage pipelineStages []*model.PipelineStage rollbackStages []*model.PipelineStage + stageStatusMap map[string]model.StageStatus } func (p *fakePlugin) Close() error { return nil } @@ -97,7 +99,18 @@ func (p *fakePlugin) FetchDefinedStages(ctx context.Context, req *deployment.Fet Stages: stages, }, nil } +func (p *fakePlugin) ExecuteStage(ctx context.Context, req *deployment.ExecuteStageRequest, opts ...grpc.CallOption) (*deployment.ExecuteStageResponse, error) { + status, ok := p.stageStatusMap[req.Input.Stage.Id] + if !ok { + return &deployment.ExecuteStageResponse{ + Status: model.StageStatus_STAGE_NOT_STARTED_YET, + }, nil + } + return &deployment.ExecuteStageResponse{ + Status: status, + }, nil +} func pointerBool(b bool) *bool { return &b } @@ -107,139 +120,190 @@ func TestBuildQuickSyncStages(t *testing.T) { testcases := []struct { name string - plugins []pluginapi.PluginClient + pluginRegistry registry.PluginRegistry cfg *config.GenericApplicationSpec wantErr bool expectedStages []*model.PipelineStage }{ { name: "only one plugin", - plugins: []pluginapi.PluginClient{ - &fakePlugin{ - quickStages: []*model.PipelineStage{ - { - Id: "plugin-1-stage-1", - }, - }, - rollbackStages: []*model.PipelineStage{ - { - Id: "plugin-1-rollback", - Rollback: true, + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "plugin-1", + Cli: &fakePlugin{ + quickStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + }, + }, + rollbackStages: []*model.PipelineStage{ + { + Id: "plugin-1-rollback", + Name: "plugin-1-rollback", + Rollback: true, + }, + }, }, }, - }, - }, + }) + require.NoError(t, err) + + return pr + }(), cfg: &config.GenericApplicationSpec{ Planner: config.DeploymentPlanner{ AutoRollback: pointerBool(true), }, + Plugins: []string{"plugin-1"}, }, wantErr: false, expectedStages: []*model.PipelineStage{ { - Id: "plugin-1-stage-1", + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", }, { Id: "plugin-1-rollback", + Name: "plugin-1-rollback", Rollback: true, }, }, }, { name: "multi plugins", - plugins: []pluginapi.PluginClient{ - &fakePlugin{ - quickStages: []*model.PipelineStage{ - { - Id: "plugin-1-stage-1", - }, - }, - rollbackStages: []*model.PipelineStage{ - { - Id: "plugin-1-rollback", - Rollback: true, - }, - }, - }, - &fakePlugin{ - quickStages: []*model.PipelineStage{ - { - Id: "plugin-2-stage-1", + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "plugin-1", + Cli: &fakePlugin{ + quickStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + }, + }, + rollbackStages: []*model.PipelineStage{ + { + Id: "plugin-1-rollback", + Name: "plugin-1-rollback", + Rollback: true, + }, + }, }, }, - rollbackStages: []*model.PipelineStage{ - { - Id: "plugin-2-rollback", - Rollback: true, + { + Name: "plugin-2", + Cli: &fakePlugin{ + quickStages: []*model.PipelineStage{ + { + Id: "plugin-2-stage-1", + Name: "plugin-2-stage-1", + }, + }, + rollbackStages: []*model.PipelineStage{ + { + Id: "plugin-2-rollback", + Name: "plugin-2-rollback", + Rollback: true, + }, + }, }, }, - }, - }, + }) + require.NoError(t, err) + + return pr + }(), cfg: &config.GenericApplicationSpec{ Planner: config.DeploymentPlanner{ AutoRollback: pointerBool(true), }, + Plugins: []string{"plugin-1", "plugin-2"}, }, wantErr: false, expectedStages: []*model.PipelineStage{ { - Id: "plugin-1-stage-1", + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", }, { - Id: "plugin-2-stage-1", + Id: "plugin-2-stage-1", + Name: "plugin-2-stage-1", }, { Id: "plugin-1-rollback", + Name: "plugin-1-rollback", Rollback: true, }, { Id: "plugin-2-rollback", + Name: "plugin-2-rollback", Rollback: true, }, }, }, { name: "multi plugins - no rollback", - plugins: []pluginapi.PluginClient{ - &fakePlugin{ - quickStages: []*model.PipelineStage{ - { - Id: "plugin-1-stage-1", - }, - }, - rollbackStages: []*model.PipelineStage{ - { - Id: "plugin-1-rollback", - Rollback: true, - }, - }, - }, - &fakePlugin{ - quickStages: []*model.PipelineStage{ - { - Id: "plugin-2-stage-1", + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "plugin-1", + Cli: &fakePlugin{ + quickStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + }, + }, + rollbackStages: []*model.PipelineStage{ + { + Id: "plugin-1-rollback", + Name: "plugin-1-rollback", + Rollback: true, + }, + }, }, }, - rollbackStages: []*model.PipelineStage{ - { - Id: "plugin-2-rollback", - Rollback: true, + { + Name: "plugin-2", + Cli: &fakePlugin{ + quickStages: []*model.PipelineStage{ + { + Id: "plugin-2-stage-1", + Name: "plugin-2-stage-1", + }, + }, + rollbackStages: []*model.PipelineStage{ + { + Id: "plugin-2-rollback", + Name: "plugin-2-rollback", + Rollback: true, + }, + }, }, }, - }, - }, + }) + require.NoError(t, err) + + return pr + }(), cfg: &config.GenericApplicationSpec{ Planner: config.DeploymentPlanner{ AutoRollback: pointerBool(false), }, + Plugins: []string{"plugin-1", "plugin-2"}, }, wantErr: false, expectedStages: []*model.PipelineStage{ { - Id: "plugin-1-stage-1", + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", }, { - Id: "plugin-2-stage-1", + Id: "plugin-2-stage-1", + Name: "plugin-2-stage-1", }, }, }, @@ -248,7 +312,7 @@ func TestBuildQuickSyncStages(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { planner := &planner{ - plugins: tc.plugins, + pluginRegistry: tc.pluginRegistry, } stages, err := planner.buildQuickSyncStages(context.TODO(), tc.cfg) require.Equal(t, tc.wantErr, err != nil) @@ -262,37 +326,45 @@ func TestBuildPipelineSyncStages(t *testing.T) { testcases := []struct { name string - plugins []pluginapi.PluginClient + pluginRegistry registry.PluginRegistry cfg *config.GenericApplicationSpec wantErr bool expectedStages []*model.PipelineStage }{ { name: "only one plugin", - plugins: []pluginapi.PluginClient{ - &fakePlugin{ - pipelineStages: []*model.PipelineStage{ - { - Id: "plugin-1-stage-1", - Index: 0, - Name: "plugin-1-stage-1", - }, - { - Id: "plugin-1-stage-2", - Index: 1, - Name: "plugin-1-stage-2", - }, - }, - rollbackStages: []*model.PipelineStage{ - { - Id: "plugin-1-rollback", - Index: 0, - Name: "plugin-1-rollback", - Rollback: true, + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "plugin-1", + Cli: &fakePlugin{ + pipelineStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Index: 0, + Name: "plugin-1-stage-1", + }, + { + Id: "plugin-1-stage-2", + Index: 1, + Name: "plugin-1-stage-2", + }, + }, + rollbackStages: []*model.PipelineStage{ + { + Id: "plugin-1-rollback", + Index: 0, + Name: "plugin-1-rollback", + Rollback: true, + }, + }, }, }, - }, - }, + }) + require.NoError(t, err) + + return pr + }(), cfg: &config.GenericApplicationSpec{ Planner: config.DeploymentPlanner{ AutoRollback: pointerBool(true), @@ -333,44 +405,60 @@ func TestBuildPipelineSyncStages(t *testing.T) { }, { name: "multi plugins single rollback", - plugins: []pluginapi.PluginClient{ - &fakePlugin{ - pipelineStages: []*model.PipelineStage{ - { - Id: "plugin-1-stage-1", - Name: "plugin-1-stage-1", - }, - { - Id: "plugin-1-stage-2", - Name: "plugin-1-stage-2", - }, - { - Id: "plugin-1-stage-3", - Name: "plugin-1-stage-3", - }, - }, - rollbackStages: []*model.PipelineStage{ - { - Id: "plugin-1-rollback", - Index: 0, - Name: "plugin-1-rollback", - Rollback: true, + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "plugin-1", + Cli: &fakePlugin{ + pipelineStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Index: 0, + Name: "plugin-1-stage-1", + }, + { + Id: "plugin-1-stage-2", + Index: 1, + Name: "plugin-1-stage-2", + }, + { + Id: "plugin-1-stage-3", + Index: 2, + Name: "plugin-1-stage-3", + }, + }, + rollbackStages: []*model.PipelineStage{ + { + Id: "plugin-1-rollback", + Index: 0, + Name: "plugin-1-rollback", + Rollback: true, + }, + }, }, }, - }, - &fakePlugin{ - pipelineStages: []*model.PipelineStage{ - { - Id: "plugin-2-stage-1", - Name: "plugin-2-stage-1", - }, - { - Id: "plugin-2-stage-2", - Name: "plugin-2-stage-2", + { + Name: "plugin-2", + Cli: &fakePlugin{ + pipelineStages: []*model.PipelineStage{ + { + Id: "plugin-2-stage-1", + Index: 0, + Name: "plugin-2-stage-1", + }, + { + Id: "plugin-2-stage-2", + Index: 1, + Name: "plugin-2-stage-2", + }, + }, }, }, - }, - }, + }) + require.NoError(t, err) + + return pr + }(), cfg: &config.GenericApplicationSpec{ Planner: config.DeploymentPlanner{ AutoRollback: pointerBool(true), @@ -441,48 +529,63 @@ func TestBuildPipelineSyncStages(t *testing.T) { }, { name: "multi plugins multi rollback", - plugins: []pluginapi.PluginClient{ - &fakePlugin{ - pipelineStages: []*model.PipelineStage{ - { - Id: "plugin-1-stage-1", - Name: "plugin-1-stage-1", - }, - { - Id: "plugin-1-stage-2", - Name: "plugin-1-stage-2", - }, - { - Id: "plugin-1-stage-3", - Name: "plugin-1-stage-3", - }, - }, - rollbackStages: []*model.PipelineStage{ - { - Id: "plugin-1-rollback", - Index: 0, - Name: "plugin-1-rollback", - Rollback: true, - }, - }, - }, - &fakePlugin{ - pipelineStages: []*model.PipelineStage{ - { - Id: "plugin-2-stage-1", - Name: "plugin-2-stage-1", + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "plugin-1", + Cli: &fakePlugin{ + pipelineStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Index: 0, + Name: "plugin-1-stage-1", + }, + { + Id: "plugin-1-stage-2", + Index: 1, + Name: "plugin-1-stage-2", + }, + { + Id: "plugin-1-stage-3", + Index: 2, + Name: "plugin-1-stage-3", + }, + }, + rollbackStages: []*model.PipelineStage{ + { + Id: "plugin-1-rollback", + Index: 0, + Name: "plugin-1-rollback", + Rollback: true, + }, + }, }, }, - rollbackStages: []*model.PipelineStage{ - { - Id: "plugin-2-rollback", - Index: 2, - Name: "plugin-2-rollback", - Rollback: true, + { + Name: "plugin-2", + Cli: &fakePlugin{ + pipelineStages: []*model.PipelineStage{ + { + Id: "plugin-2-stage-1", + Index: 0, + Name: "plugin-2-stage-1", + }, + }, + rollbackStages: []*model.PipelineStage{ + { + Id: "plugin-2-rollback", + Index: 2, + Name: "plugin-2-rollback", + Rollback: true, + }, + }, }, }, - }, - }, + }) + require.NoError(t, err) + + return pr + }(), cfg: &config.GenericApplicationSpec{ Planner: config.DeploymentPlanner{ AutoRollback: pointerBool(true), @@ -551,16 +654,8 @@ func TestBuildPipelineSyncStages(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - stageBasedPluginMap := make(map[string]pluginapi.PluginClient) - for _, p := range tc.plugins { - stages, _ := p.FetchDefinedStages(context.TODO(), &deployment.FetchDefinedStagesRequest{}) - for _, s := range stages.Stages { - stageBasedPluginMap[s] = p - } - } planner := &planner{ - plugins: tc.plugins, - stageBasedPluginsMap: stageBasedPluginMap, + pluginRegistry: tc.pluginRegistry, } stages, err := planner.buildPipelineSyncStages(context.TODO(), tc.cfg) require.Equal(t, tc.wantErr, err != nil) @@ -576,6 +671,7 @@ func TestPlanner_BuildPlan(t *testing.T) { name string isFirstDeploy bool plugins []pluginapi.PluginClient + pluginRegistry registry.PluginRegistry cfg *config.GenericApplicationSpec deployment *model.Deployment wantErr bool @@ -584,20 +680,30 @@ func TestPlanner_BuildPlan(t *testing.T) { { name: "quick sync strategy triggered by web console", isFirstDeploy: false, - plugins: []pluginapi.PluginClient{ - &fakePlugin{ - quickStages: []*model.PipelineStage{ - { - Id: "plugin-1-stage-1", - Visible: true, + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "plugin-1", + Cli: &fakePlugin{ + quickStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + Visible: true, + }, + }, }, }, - }, - }, + }) + require.NoError(t, err) + + return pr + }(), cfg: &config.GenericApplicationSpec{ Planner: config.DeploymentPlanner{ AutoRollback: pointerBool(true), }, + Plugins: []string{"plugin-1"}, }, deployment: &model.Deployment{ Trigger: &model.DeploymentTrigger{ @@ -612,6 +718,7 @@ func TestPlanner_BuildPlan(t *testing.T) { Stages: []*model.PipelineStage{ { Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", Visible: true, }, }, @@ -626,17 +733,26 @@ func TestPlanner_BuildPlan(t *testing.T) { { name: "pipeline sync strategy triggered by web console", isFirstDeploy: false, - plugins: []pluginapi.PluginClient{ - &fakePlugin{ - pipelineStages: []*model.PipelineStage{ - { - Id: "plugin-1-stage-1", - Name: "plugin-1-stage-1", - Visible: true, + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "plugin-1", + Cli: &fakePlugin{ + pipelineStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Index: 0, + Name: "plugin-1-stage-1", + Visible: true, + }, + }, }, }, - }, - }, + }) + require.NoError(t, err) + + return pr + }(), cfg: &config.GenericApplicationSpec{ Planner: config.DeploymentPlanner{ AutoRollback: pointerBool(true), @@ -679,20 +795,30 @@ func TestPlanner_BuildPlan(t *testing.T) { { name: "quick sync due to no pipeline configured", isFirstDeploy: false, - plugins: []pluginapi.PluginClient{ - &fakePlugin{ - quickStages: []*model.PipelineStage{ - { - Id: "plugin-1-stage-1", - Visible: true, + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "plugin-1", + Cli: &fakePlugin{ + quickStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + Visible: true, + }, + }, }, }, - }, - }, + }) + require.NoError(t, err) + + return pr + }(), cfg: &config.GenericApplicationSpec{ Planner: config.DeploymentPlanner{ AutoRollback: pointerBool(true), }, + Plugins: []string{"plugin-1"}, }, deployment: &model.Deployment{ Trigger: &model.DeploymentTrigger{}, @@ -704,6 +830,7 @@ func TestPlanner_BuildPlan(t *testing.T) { Stages: []*model.PipelineStage{ { Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", Visible: true, }, }, @@ -718,17 +845,26 @@ func TestPlanner_BuildPlan(t *testing.T) { { name: "pipeline sync due to alwaysUsePipeline", isFirstDeploy: false, - plugins: []pluginapi.PluginClient{ - &fakePlugin{ - pipelineStages: []*model.PipelineStage{ - { - Id: "plugin-1-stage-1", - Name: "plugin-1-stage-1", - Visible: true, + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "plugin-1", + Cli: &fakePlugin{ + pipelineStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Index: 0, + Name: "plugin-1-stage-1", + Visible: true, + }, + }, }, }, - }, - }, + }) + require.NoError(t, err) + + return pr + }(), cfg: &config.GenericApplicationSpec{ Planner: config.DeploymentPlanner{ AlwaysUsePipeline: true, @@ -769,16 +905,25 @@ func TestPlanner_BuildPlan(t *testing.T) { { name: "quick sync due to first deployment", isFirstDeploy: true, - plugins: []pluginapi.PluginClient{ - &fakePlugin{ - quickStages: []*model.PipelineStage{ - { - Id: "plugin-1-stage-1", - Visible: true, + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "plugin-1", + Cli: &fakePlugin{ + quickStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", + Visible: true, + }, + }, }, }, - }, - }, + }) + require.NoError(t, err) + + return pr + }(), cfg: &config.GenericApplicationSpec{ Planner: config.DeploymentPlanner{ AutoRollback: pointerBool(true), @@ -802,6 +947,7 @@ func TestPlanner_BuildPlan(t *testing.T) { Stages: []*model.PipelineStage{ { Id: "plugin-1-stage-1", + Name: "plugin-1-stage-1", Visible: true, }, }, @@ -837,6 +983,36 @@ func TestPlanner_BuildPlan(t *testing.T) { }, }, }, + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "plugin-1", + Cli: &fakePlugin{ + syncStrategy: &deployment.DetermineStrategyResponse{ + SyncStrategy: model.SyncStrategy_PIPELINE, + Summary: "determined by plugin", + }, + pipelineStages: []*model.PipelineStage{ + { + Id: "plugin-1-stage-1", + Index: 0, + Name: "plugin-1-stage-1", + Visible: true, + }, + }, + quickStages: []*model.PipelineStage{ + { + Id: "plugin-1-quick-stage-1", + Visible: true, + }, + }, + }, + }, + }) + require.NoError(t, err) + + return pr + }(), cfg: &config.GenericApplicationSpec{ Planner: config.DeploymentPlanner{ AutoRollback: pointerBool(true), @@ -877,16 +1053,8 @@ func TestPlanner_BuildPlan(t *testing.T) { for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - stageBasedPluginMap := make(map[string]pluginapi.PluginClient) - for _, p := range tc.plugins { - stages, _ := p.FetchDefinedStages(context.TODO(), &deployment.FetchDefinedStagesRequest{}) - for _, s := range stages.Stages { - stageBasedPluginMap[s] = p - } - } planner := &planner{ - plugins: tc.plugins, - stageBasedPluginsMap: stageBasedPluginMap, + pluginRegistry: tc.pluginRegistry, deployment: tc.deployment, lastSuccessfulCommitHash: "", lastSuccessfulConfigFilename: "", diff --git a/pkg/app/pipedv1/controller/scheduler.go b/pkg/app/pipedv1/controller/scheduler.go index 5b3bf4e636..58146848f0 100644 --- a/pkg/app/pipedv1/controller/scheduler.go +++ b/pkg/app/pipedv1/controller/scheduler.go @@ -34,8 +34,8 @@ import ( "github.com/pipe-cd/pipecd/pkg/app/server/service/pipedservice" config "github.com/pipe-cd/pipecd/pkg/configv1" "github.com/pipe-cd/pipecd/pkg/model" - pluginapi "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1" "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1/deployment" + "github.com/pipe-cd/pipecd/pkg/plugin/registry" ) // scheduler is a dedicated object for a specific deployment of a single application. @@ -43,7 +43,7 @@ type scheduler struct { deployment *model.Deployment workingDir string - stageBasedPluginsMap map[string]pluginapi.PluginClient + pluginRegistry registry.PluginRegistry apiClient apiClient gitClient gitClient @@ -79,7 +79,7 @@ func newScheduler( workingDir string, apiClient apiClient, gitClient gitClient, - stageBasedPluginsMap map[string]pluginapi.PluginClient, + pluginRegistry registry.PluginRegistry, notifier notifier, secretsDecrypter secretDecrypter, logger *zap.Logger, @@ -96,9 +96,9 @@ func newScheduler( s := &scheduler{ deployment: d, workingDir: workingDir, - stageBasedPluginsMap: stageBasedPluginsMap, apiClient: apiClient, gitClient: gitClient, + pluginRegistry: pluginRegistry, metadataStore: metadatastore.NewMetadataStore(apiClient, d), notifier: notifier, secretDecrypter: secretsDecrypter, @@ -513,9 +513,9 @@ func (s *scheduler) executeStage(sig StopSignal, ps *model.PipelineStage) (final } // Find the executor plugin for this stage. - plugin, ok := s.stageBasedPluginsMap[ps.Name] - if !ok { - s.logger.Error("failed to find the plugin for the stage", zap.String("stage-name", ps.Name)) + plugin, err := s.pluginRegistry.GetPluginClientByStageName(ps.Name) + if err != nil { + s.logger.Error("failed to find the plugin for the stage", zap.String("stage-name", ps.Name), zap.Error(err)) s.reportStageStatus(ctx, ps.Id, model.StageStatus_STAGE_FAILURE, ps.Requires) return model.StageStatus_STAGE_FAILURE } diff --git a/pkg/app/pipedv1/controller/scheduler_test.go b/pkg/app/pipedv1/controller/scheduler_test.go index b73bd92f7e..518b28863f 100644 --- a/pkg/app/pipedv1/controller/scheduler_test.go +++ b/pkg/app/pipedv1/controller/scheduler_test.go @@ -21,6 +21,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "google.golang.org/grpc" @@ -30,6 +31,7 @@ import ( "github.com/pipe-cd/pipecd/pkg/model" pluginapi "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1" "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1/deployment" + "github.com/pipe-cd/pipecd/pkg/plugin/registry" ) func TestDetermineStageStatus(t *testing.T) { @@ -215,9 +217,27 @@ func TestExecuteStage(t *testing.T) { apiClient: &fakeAPIClient{}, targetDSP: &fakeDeploySourceProvider{}, runningDSP: &fakeDeploySourceProvider{}, - stageBasedPluginsMap: map[string]pluginapi.PluginClient{ - "stage-name": &fakeExecutorPluginClient{}, - }, + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "stage-name", + Cli: &fakePlugin{ + pipelineStages: []*model.PipelineStage{ + { + Id: "stage-id", + Name: "stage-name", + }, + }, + stageStatusMap: map[string]model.StageStatus{ + "stage-id": model.StageStatus_STAGE_SUCCESS, + }, + }, + }, + }) + require.NoError(t, err) + + return pr + }(), genericApplicationConfig: &config.GenericApplicationSpec{ Pipeline: &config.DeploymentPipeline{ Stages: []config.PipelineStage{ @@ -257,9 +277,27 @@ func TestExecuteStage_SignalTerminated(t *testing.T) { apiClient: &fakeAPIClient{}, targetDSP: &fakeDeploySourceProvider{}, runningDSP: &fakeDeploySourceProvider{}, - stageBasedPluginsMap: map[string]pluginapi.PluginClient{ - "stage-name": &fakeExecutorPluginClient{}, - }, + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "stage-name", + Cli: &fakePlugin{ + pipelineStages: []*model.PipelineStage{ + { + Id: "stage-id", + Name: "stage-name", + }, + }, + stageStatusMap: map[string]model.StageStatus{ + "stage-id": model.StageStatus_STAGE_SUCCESS, + }, + }, + }, + }) + require.NoError(t, err) + + return pr + }(), genericApplicationConfig: &config.GenericApplicationSpec{ Pipeline: &config.DeploymentPipeline{ Stages: []config.PipelineStage{ @@ -295,9 +333,27 @@ func TestExecuteStage_SignalCancelled(t *testing.T) { apiClient: &fakeAPIClient{}, targetDSP: &fakeDeploySourceProvider{}, runningDSP: &fakeDeploySourceProvider{}, - stageBasedPluginsMap: map[string]pluginapi.PluginClient{ - "stage-name": &fakeExecutorPluginClient{}, - }, + pluginRegistry: func() registry.PluginRegistry { + pr, err := registry.NewPluginRegistry(context.TODO(), []registry.Plugin{ + { + Name: "stage-name", + Cli: &fakePlugin{ + pipelineStages: []*model.PipelineStage{ + { + Id: "stage-id", + Name: "stage-name", + }, + }, + stageStatusMap: map[string]model.StageStatus{ + "stage-id": model.StageStatus_STAGE_SUCCESS, + }, + }, + }, + }) + require.NoError(t, err) + + return pr + }(), genericApplicationConfig: &config.GenericApplicationSpec{ Pipeline: &config.DeploymentPipeline{ Stages: []config.PipelineStage{ diff --git a/pkg/configv1/application.go b/pkg/configv1/application.go index f1d69f0a04..46cd0801ec 100644 --- a/pkg/configv1/application.go +++ b/pkg/configv1/application.go @@ -58,6 +58,8 @@ type GenericApplicationSpec struct { EventWatcher []EventWatcherConfig `json:"eventWatcher"` // Configuration for drift detection DriftDetection *DriftDetection `json:"driftDetection"` + // List of the plugin name + Plugins []string `json:"plugins"` } type DeploymentPlanner struct { diff --git a/pkg/plugin/registry/registry.go b/pkg/plugin/registry/registry.go new file mode 100644 index 0000000000..ef44396e1f --- /dev/null +++ b/pkg/plugin/registry/registry.go @@ -0,0 +1,141 @@ +// Copyright 2024 The PipeCD Authors. +// +// 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 planner provides a piped component +// that decides the deployment pipeline of a deployment. +// The planner bases on the changes from git commits +// then builds the deployment manifests to know the behavior of the deployment. +// From that behavior the planner can decides which pipeline should be applied. +package registry + +import ( + "context" + "fmt" + + config "github.com/pipe-cd/pipecd/pkg/configv1" + pluginapi "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1" + "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1/deployment" +) + +// Plugin represents a plugin with its name and client. +type Plugin struct { + Name string + Cli pluginapi.PluginClient +} + +// PluginRegistry is the interface that provides methods to get plugin clients. +type PluginRegistry interface { + GetPluginClientByStageName(name string) (pluginapi.PluginClient, error) + GetPluginClientsByAppConfig(cfg *config.GenericApplicationSpec) ([]pluginapi.PluginClient, error) +} + +type pluginRegistry struct { + nameBasedPlugins map[string]pluginapi.PluginClient // key: plugin name + stageBasedPlugins map[string]pluginapi.PluginClient // key: stage name + + // TODO: add more fields if needed (e.g. deploymentBasedPlugins, livestateBasedPlugins) +} + +// NewPluginRegistry creates a new PluginRegistry based on the given plugins. +func NewPluginRegistry(ctx context.Context, plugins []Plugin) (PluginRegistry, error) { + nameBasedPlugins := make(map[string]pluginapi.PluginClient) + stageBasedPlugins := make(map[string]pluginapi.PluginClient) + + for _, plg := range plugins { + // add the plugin to the name-based plugins + nameBasedPlugins[plg.Name] = plg.Cli + + // add the plugin to the stage-based plugins + res, err := plg.Cli.FetchDefinedStages(ctx, &deployment.FetchDefinedStagesRequest{}) + if err != nil { + return nil, err + } + + for _, stage := range res.Stages { + stageBasedPlugins[stage] = plg.Cli + } + } + + return &pluginRegistry{ + nameBasedPlugins: nameBasedPlugins, + stageBasedPlugins: stageBasedPlugins, + }, nil +} + +// GetPluginClientByStageName returns the plugin client based on the given stage name. +func (pr *pluginRegistry) GetPluginClientByStageName(name string) (pluginapi.PluginClient, error) { + plugin, ok := pr.stageBasedPlugins[name] + if !ok { + return nil, fmt.Errorf("no plugin found for the specified stage") + } + + return plugin, nil +} + +// GetPluginClientsByAppConfig returns the plugin clients based on the given configuration. +// The priority of determining plugins is as follows: +// 1. If the pipeline is specified, it will determine the plugins based on the pipeline stages. +// 2. If the plugins are specified, it will determine the plugins based on the plugin names. +// 3. If neither the pipeline nor the plugins are specified, it will return an error. +func (pr *pluginRegistry) GetPluginClientsByAppConfig(cfg *config.GenericApplicationSpec) ([]pluginapi.PluginClient, error) { + if cfg.Pipeline != nil && len(cfg.Pipeline.Stages) > 0 { + return pr.getPluginClientsByPipeline(cfg.Pipeline) + } + + if cfg.Plugins != nil { + return pr.getPluginClientsByNames(cfg.Plugins) + } + + return nil, fmt.Errorf("no plugin specified") +} + +func (pr *pluginRegistry) getPluginClientsByPipeline(pipeline *config.DeploymentPipeline) ([]pluginapi.PluginClient, error) { + if len(pipeline.Stages) == 0 { + return nil, fmt.Errorf("no stages are set in the pipeline") + } + + plugins := make([]pluginapi.PluginClient, 0, len(pipeline.Stages)) + for _, stage := range pipeline.Stages { + plugin, ok := pr.stageBasedPlugins[stage.Name.String()] + if ok { + plugins = append(plugins, plugin) + } + } + + if len(plugins) == 0 { + return nil, fmt.Errorf("no plugin found for each stages") + } + + return plugins, nil +} + +func (pr *pluginRegistry) getPluginClientsByNames(names []string) ([]pluginapi.PluginClient, error) { + if len(names) == 0 { + return nil, fmt.Errorf("no plugin names are set") + } + + plugins := make([]pluginapi.PluginClient, 0, len(names)) + for _, name := range names { + plugin, ok := pr.nameBasedPlugins[name] + if ok { + plugins = append(plugins, plugin) + } + } + + if len(plugins) == 0 { + return nil, fmt.Errorf("no plugin found for the given plugin names") + } + + return plugins, nil +} diff --git a/pkg/plugin/registry/registry_test.go b/pkg/plugin/registry/registry_test.go new file mode 100644 index 0000000000..98afd5271d --- /dev/null +++ b/pkg/plugin/registry/registry_test.go @@ -0,0 +1,315 @@ +// Copyright 2024 The PipeCD Authors. +// +// 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 planner provides a piped component +// that decides the deployment pipeline of a deployment. +// The planner bases on the changes from git commits +// then builds the deployment manifests to know the behavior of the deployment. +// From that behavior the planner can decides which pipeline should be applied. +package registry + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + config "github.com/pipe-cd/pipecd/pkg/configv1" + pluginapi "github.com/pipe-cd/pipecd/pkg/plugin/api/v1alpha1" +) + +type fakePluginClient struct { + pluginapi.PluginClient + name string +} + +func TestPluginRegistry_GetPluginClientsByAppConfig(t *testing.T) { + tests := []struct { + name string + cfg *config.GenericApplicationSpec + setup func() *pluginRegistry + expected []pluginapi.PluginClient + wantErr bool + }{ + { + name: "get plugins by pipeline", + cfg: &config.GenericApplicationSpec{ + Pipeline: &config.DeploymentPipeline{ + Stages: []config.PipelineStage{ + {Name: "stage1"}, + {Name: "stage2"}, + }, + }, + Plugins: nil, + }, + setup: func() *pluginRegistry { + return &pluginRegistry{ + stageBasedPlugins: map[string]pluginapi.PluginClient{ + "stage1": fakePluginClient{name: "stage1"}, + "stage2": fakePluginClient{name: "stage2"}, + }, + } + }, + expected: []pluginapi.PluginClient{ + fakePluginClient{name: "stage1"}, + fakePluginClient{name: "stage2"}, + }, + wantErr: false, + }, + { + name: "get plugins by plugin names", + cfg: &config.GenericApplicationSpec{ + Pipeline: nil, + Plugins: []string{"plugin1", "plugin2"}, + }, + setup: func() *pluginRegistry { + return &pluginRegistry{ + nameBasedPlugins: map[string]pluginapi.PluginClient{ + "plugin1": fakePluginClient{name: "plugin1"}, + "plugin2": fakePluginClient{name: "plugin2"}, + }, + } + }, + expected: []pluginapi.PluginClient{ + fakePluginClient{name: "plugin1"}, + fakePluginClient{name: "plugin2"}, + }, + wantErr: false, + }, + { + name: "get plugins by pipeline when both pipeline and plugins are specified", + cfg: &config.GenericApplicationSpec{ + Pipeline: &config.DeploymentPipeline{ + Stages: []config.PipelineStage{ + {Name: "stage1"}, + {Name: "stage2"}, + }, + }, + Plugins: []string{"plugin1", "plugin2"}, + }, + setup: func() *pluginRegistry { + return &pluginRegistry{ + stageBasedPlugins: map[string]pluginapi.PluginClient{ + "stage1": fakePluginClient{name: "stage1"}, + "stage2": fakePluginClient{name: "stage2"}, + }, + nameBasedPlugins: map[string]pluginapi.PluginClient{ + "plugin1": fakePluginClient{name: "plugin1"}, + "plugin2": fakePluginClient{name: "plugin2"}, + }, + } + }, + expected: []pluginapi.PluginClient{ + fakePluginClient{name: "stage1"}, + fakePluginClient{name: "stage2"}, + }, + wantErr: false, + }, + { + name: "no plugins found when no pipeline or plugins specified", + cfg: &config.GenericApplicationSpec{}, + setup: func() *pluginRegistry { + return &pluginRegistry{ + nameBasedPlugins: map[string]pluginapi.PluginClient{}, + } + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pr := tt.setup() + plugins, err := pr.GetPluginClientsByAppConfig(tt.cfg) + assert.Equal(t, tt.expected, plugins) + assert.Equal(t, tt.wantErr, err != nil) + }) + } +} +func TestPluginRegistry_getPluginClientsByPipeline(t *testing.T) { + tests := []struct { + name string + pipeline *config.DeploymentPipeline + setup func() *pluginRegistry + expected []pluginapi.PluginClient + wantErr bool + }{ + { + name: "get plugins by valid pipeline stages", + pipeline: &config.DeploymentPipeline{ + Stages: []config.PipelineStage{ + {Name: "stage1"}, + {Name: "stage2"}, + }, + }, + setup: func() *pluginRegistry { + return &pluginRegistry{ + stageBasedPlugins: map[string]pluginapi.PluginClient{ + "stage1": fakePluginClient{name: "stage1"}, + "stage2": fakePluginClient{name: "stage2"}, + }, + } + }, + expected: []pluginapi.PluginClient{ + fakePluginClient{name: "stage1"}, + fakePluginClient{name: "stage2"}, + }, + wantErr: false, + }, + { + name: "no plugins found for empty pipeline stages", + pipeline: &config.DeploymentPipeline{ + Stages: []config.PipelineStage{}, + }, + setup: func() *pluginRegistry { + return &pluginRegistry{ + stageBasedPlugins: map[string]pluginapi.PluginClient{}, + } + }, + expected: nil, + wantErr: true, + }, + { + name: "no plugins found for non-existent pipeline stages", + pipeline: &config.DeploymentPipeline{ + Stages: []config.PipelineStage{ + {Name: "stage1"}, + {Name: "stage2"}, + }, + }, + setup: func() *pluginRegistry { + return &pluginRegistry{ + stageBasedPlugins: map[string]pluginapi.PluginClient{}, + } + }, + expected: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pr := tt.setup() + plugins, err := pr.getPluginClientsByPipeline(tt.pipeline) + assert.Equal(t, tt.expected, plugins) + assert.Equal(t, tt.wantErr, err != nil) + }) + } +} +func TestPluginRegistry_getPluginClientsByNames(t *testing.T) { + tests := []struct { + name string + pluginNames []string + setup func() *pluginRegistry + expected []pluginapi.PluginClient + wantErr bool + }{ + { + name: "get plugins by valid plugin names", + pluginNames: []string{"plugin1", "plugin2"}, + setup: func() *pluginRegistry { + return &pluginRegistry{ + nameBasedPlugins: map[string]pluginapi.PluginClient{ + "plugin1": fakePluginClient{name: "plugin1"}, + "plugin2": fakePluginClient{name: "plugin2"}, + }, + } + }, + expected: []pluginapi.PluginClient{ + fakePluginClient{name: "plugin1"}, + fakePluginClient{name: "plugin2"}, + }, + wantErr: false, + }, + { + name: "no plugins found for empty plugin names", + pluginNames: []string{}, + setup: func() *pluginRegistry { + return &pluginRegistry{ + nameBasedPlugins: map[string]pluginapi.PluginClient{ + "plugin1": fakePluginClient{name: "plugin1"}, + "plugin2": fakePluginClient{name: "plugin2"}, + }, + } + }, + wantErr: true, + }, + { + name: "no plugins found for non-existent plugin names", + pluginNames: []string{"plugin1", "plugin2"}, + setup: func() *pluginRegistry { + return &pluginRegistry{ + nameBasedPlugins: map[string]pluginapi.PluginClient{ + "plugin3": fakePluginClient{name: "plugin3"}, + "plugin4": fakePluginClient{name: "plugin4"}, + }, + } + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pr := tt.setup() + plugins, err := pr.getPluginClientsByNames(tt.pluginNames) + assert.Equal(t, tt.expected, plugins) + assert.Equal(t, tt.wantErr, err != nil) + }) + } +} +func TestPluginRegistry_GetPluginClientByStageName(t *testing.T) { + tests := []struct { + name string + stage string + setup func() *pluginRegistry + expected pluginapi.PluginClient + wantErr bool + }{ + { + name: "get plugin by valid stage name", + stage: "stage1", + setup: func() *pluginRegistry { + return &pluginRegistry{ + stageBasedPlugins: map[string]pluginapi.PluginClient{ + "stage1": fakePluginClient{name: "stage1"}, + }, + } + }, + expected: fakePluginClient{name: "stage1"}, + wantErr: false, + }, + { + name: "no plugin found for non-existent stage name", + stage: "stage2", + setup: func() *pluginRegistry { + return &pluginRegistry{ + stageBasedPlugins: map[string]pluginapi.PluginClient{ + "stage1": fakePluginClient{name: "stage1"}, + }, + } + }, + expected: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pr := tt.setup() + plugin, err := pr.GetPluginClientByStageName(tt.stage) + assert.Equal(t, tt.expected, plugin) + assert.Equal(t, tt.wantErr, err != nil) + }) + } +}