diff --git a/pkg/app/api/grpcapi/api.go b/pkg/app/api/grpcapi/api.go index 2316c60538..3fc15d5d91 100644 --- a/pkg/app/api/grpcapi/api.go +++ b/pkg/app/api/grpcapi/api.go @@ -466,7 +466,7 @@ func (a *API) GetPlanPreviewResults(ctx context.Context, req *apiservice.GetPlan } } - results := make([]*model.ApplicationPlanPreviewResult, 0, len(req.Commands)) + results := make([]*model.PlanPreviewCommandResult, 0, len(req.Commands)) // Fetch ouput data to build results. for _, commandID := range req.Commands { @@ -474,9 +474,9 @@ func (a *API) GetPlanPreviewResults(ctx context.Context, req *apiservice.GetPlan if err != nil { return nil, status.Error(codes.Internal, fmt.Sprintf("Failed to retrieve output data of command %s", commandID)) } - var result model.ApplicationPlanPreviewResult + var result model.PlanPreviewCommandResult if err := json.Unmarshal(data, &result); err != nil { - a.logger.Error("failed to unmarshal applicaiton plan preview result", + a.logger.Error("failed to unmarshal planpreview command result", zap.String("command", commandID), zap.Error(err), ) diff --git a/pkg/app/api/service/apiservice/service.proto b/pkg/app/api/service/apiservice/service.proto index 6e6a4376a3..5c1a306d43 100644 --- a/pkg/app/api/service/apiservice/service.proto +++ b/pkg/app/api/service/apiservice/service.proto @@ -126,5 +126,5 @@ message GetPlanPreviewResultsRequest { } message GetPlanPreviewResultsResponse { - repeated pipe.model.ApplicationPlanPreviewResult results = 1; + repeated pipe.model.PlanPreviewCommandResult results = 1; } diff --git a/pkg/app/pipectl/cmd/planpreview/planpreview.go b/pkg/app/pipectl/cmd/planpreview/planpreview.go index e8753ee881..72aaa16c1c 100644 --- a/pkg/app/pipectl/cmd/planpreview/planpreview.go +++ b/pkg/app/pipectl/cmd/planpreview/planpreview.go @@ -17,7 +17,6 @@ package planpreview import ( "context" "fmt" - "io" "time" "github.com/spf13/cobra" @@ -35,11 +34,11 @@ type command struct { headBranch string headCommit string baseBranch string + out string timeout time.Duration checkInterval time.Duration clientOptions *client.Options - stdout io.Writer } func NewCommand() *cobra.Command { @@ -60,11 +59,13 @@ func NewCommand() *cobra.Command { cmd.Flags().StringVar(&c.headBranch, "head-branch", c.headBranch, "The head branch of the change.") cmd.Flags().StringVar(&c.headCommit, "head-commit", c.headCommit, "The SHA of the head commit.") cmd.Flags().StringVar(&c.baseBranch, "base-branch", c.baseBranch, "The base branch of the change.") + cmd.Flags().StringVar(&c.out, "out", c.out, "Write planpreview result to the given path.") cmd.MarkFlagRequired("repo-remote-url") cmd.MarkFlagRequired("head-branch") cmd.MarkFlagRequired("head-commit") cmd.MarkFlagRequired("base-branch") + cmd.MarkFlagRequired("out") return cmd } @@ -88,18 +89,18 @@ func (c *command) run(ctx context.Context, _ cli.Telemetry) error { resp, err := cli.RequestPlanPreview(ctx, req) if err != nil { - fmt.Fprintf(c.stdout, "Failed to request plan preview: %v", err) + fmt.Printf("Failed to request plan preview: %v\n", err) return err } - getResults := func(commands []string) ([]*model.ApplicationPlanPreviewResult, error) { + getResults := func(commands []string) ([]*model.PlanPreviewCommandResult, error) { req := &apiservice.GetPlanPreviewResultsRequest{ Commands: commands, } resp, err := cli.GetPlanPreviewResults(ctx, req) if err != nil { - fmt.Fprintf(c.stdout, "Failed to get plan preview results: %v", err) + fmt.Printf("Failed to get plan preview results: %v", err) return nil, err } @@ -129,7 +130,8 @@ func (c *command) run(ctx context.Context, _ cli.Telemetry) error { return nil } -func (c *command) printResults(results []*model.ApplicationPlanPreviewResult) error { +func (c *command) printResults(results []*model.PlanPreviewCommandResult) error { // TODO: Format preview results and support writing the result into file. + fmt.Println(results) return nil } diff --git a/pkg/app/piped/apistore/commandstore/store.go b/pkg/app/piped/apistore/commandstore/store.go index b9196357ca..8b8ad5f714 100644 --- a/pkg/app/piped/apistore/commandstore/store.go +++ b/pkg/app/piped/apistore/commandstore/store.go @@ -42,6 +42,7 @@ type Lister interface { ListApplicationCommands() []model.ReportableCommand ListDeploymentCommands() []model.ReportableCommand ListStageCommands(deploymentID, stageID string) []model.ReportableCommand + ListBuildPlanPreviewCommands() []model.ReportableCommand } type store struct { @@ -52,6 +53,7 @@ type store struct { applicationCommands []model.ReportableCommand deploymentCommands []model.ReportableCommand stageCommands []model.ReportableCommand + planPreviewCommands []model.ReportableCommand handledCommands map[string]time.Time mu sync.RWMutex gracePeriod time.Duration @@ -116,6 +118,7 @@ func (s *store) sync(ctx context.Context) error { applicationCommands = make([]model.ReportableCommand, 0) deploymentCommands = make([]model.ReportableCommand, 0) stageCommands = make([]model.ReportableCommand, 0) + planPreviewCommands = make([]model.ReportableCommand, 0) ) for _, cmd := range resp.Commands { switch cmd.Type { @@ -125,6 +128,8 @@ func (s *store) sync(ctx context.Context) error { deploymentCommands = append(deploymentCommands, s.makeReportableCommand(cmd)) case model.Command_APPROVE_STAGE: stageCommands = append(stageCommands, s.makeReportableCommand(cmd)) + case model.Command_BUILD_PLAN_PREVIEW: + planPreviewCommands = append(planPreviewCommands, s.makeReportableCommand(cmd)) } } @@ -132,6 +137,7 @@ func (s *store) sync(ctx context.Context) error { s.applicationCommands = applicationCommands s.deploymentCommands = deploymentCommands s.stageCommands = stageCommands + s.planPreviewCommands = planPreviewCommands s.mu.Unlock() return nil @@ -199,6 +205,20 @@ func (s *store) ListStageCommands(deploymentID, stageID string) []model.Reportab return commands } +func (s *store) ListBuildPlanPreviewCommands() []model.ReportableCommand { + s.mu.RLock() + defer s.mu.RUnlock() + + commands := make([]model.ReportableCommand, 0, len(s.planPreviewCommands)) + for _, cmd := range s.planPreviewCommands { + if _, ok := s.handledCommands[cmd.Id]; ok { + continue + } + commands = append(commands, cmd) + } + return commands +} + func (s *store) makeReportableCommand(c *model.Command) model.ReportableCommand { return model.ReportableCommand{ Command: c, diff --git a/pkg/app/piped/cmd/piped/BUILD.bazel b/pkg/app/piped/cmd/piped/BUILD.bazel index 99ce63a0b0..108aa968e3 100644 --- a/pkg/app/piped/cmd/piped/BUILD.bazel +++ b/pkg/app/piped/cmd/piped/BUILD.bazel @@ -23,6 +23,7 @@ go_library( "//pkg/app/piped/livestatestore:go_default_library", "//pkg/app/piped/notifier:go_default_library", "//pkg/app/piped/planner/registry:go_default_library", + "//pkg/app/piped/planpreview:go_default_library", "//pkg/app/piped/statsreporter:go_default_library", "//pkg/app/piped/toolregistry:go_default_library", "//pkg/app/piped/trigger:go_default_library", diff --git a/pkg/app/piped/cmd/piped/piped.go b/pkg/app/piped/cmd/piped/piped.go index fa2427b170..9c320c37d5 100644 --- a/pkg/app/piped/cmd/piped/piped.go +++ b/pkg/app/piped/cmd/piped/piped.go @@ -48,6 +48,7 @@ import ( "github.com/pipe-cd/pipe/pkg/app/piped/livestatereporter" "github.com/pipe-cd/pipe/pkg/app/piped/livestatestore" "github.com/pipe-cd/pipe/pkg/app/piped/notifier" + "github.com/pipe-cd/pipe/pkg/app/piped/planpreview" "github.com/pipe-cd/pipe/pkg/app/piped/statsreporter" "github.com/pipe-cd/pipe/pkg/app/piped/toolregistry" "github.com/pipe-cd/pipe/pkg/app/piped/trigger" @@ -75,6 +76,7 @@ type piped struct { toolsDir string enableDefaultKubernetesCloudProvider bool useFakeAPIClient bool + enablePlanPreview bool gracePeriod time.Duration addLoginUserToPasswd bool } @@ -105,6 +107,7 @@ func NewCommand() *cobra.Command { cmd.Flags().BoolVar(&p.useFakeAPIClient, "use-fake-api-client", p.useFakeAPIClient, "Whether the fake api client should be used instead of the real one or not.") cmd.Flags().BoolVar(&p.enableDefaultKubernetesCloudProvider, "enable-default-kubernetes-cloud-provider", p.enableDefaultKubernetesCloudProvider, "Whether the default kubernetes provider is enabled or not.") cmd.Flags().BoolVar(&p.addLoginUserToPasswd, "add-login-user-to-passwd", p.addLoginUserToPasswd, "Whether to add login user to $HOME/passwd. This is typically for applications running as a random user ID.") + cmd.Flags().BoolVar(&p.enablePlanPreview, "enable-plan-preview", p.enablePlanPreview, "A temporary flag to enable planpreview feature.") cmd.Flags().DurationVar(&p.gracePeriod, "grace-period", p.gracePeriod, "How long to wait for graceful shutdown.") cmd.MarkFlagRequired("config-file") @@ -368,6 +371,20 @@ func (p *piped) run(ctx context.Context, t cli.Telemetry) (runErr error) { }) } + if p.enablePlanPreview { + // Start running planpreview handler. + h := planpreview.NewHandler( + gitClient, + commandLister, + applicationLister, + cfg, + planpreview.WithLogger(t.Logger), + ) + group.Go(func() error { + return h.Run(ctx) + }) + } + // Wait until all piped components have finished. // A terminating signal or a finish of any components // could trigger the finish of piped. diff --git a/pkg/app/piped/planpreview/BUILD.bazel b/pkg/app/piped/planpreview/BUILD.bazel index f6b61e5079..5827d1d77c 100644 --- a/pkg/app/piped/planpreview/BUILD.bazel +++ b/pkg/app/piped/planpreview/BUILD.bazel @@ -1,9 +1,31 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", - srcs = ["planpreview.go"], + srcs = [ + "builder.go", + "handler.go", + ], importpath = "github.com/pipe-cd/pipe/pkg/app/piped/planpreview", visibility = ["//visibility:public"], - deps = ["//pkg/model:go_default_library"], + deps = [ + "//pkg/config:go_default_library", + "//pkg/git:go_default_library", + "//pkg/model:go_default_library", + "@org_uber_go_zap//:go_default_library", + ], +) + +go_test( + name = "go_default_test", + size = "small", + srcs = [ + "builder_test.go", + "handler_test.go", + ], + embed = [":go_default_library"], + deps = [ + "//pkg/model:go_default_library", + "@com_github_stretchr_testify//require:go_default_library", + ], ) diff --git a/pkg/app/piped/planpreview/builder.go b/pkg/app/piped/planpreview/builder.go new file mode 100644 index 0000000000..dbb6c49267 --- /dev/null +++ b/pkg/app/piped/planpreview/builder.go @@ -0,0 +1,97 @@ +// Copyright 2021 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 planpreview + +import ( + "context" + "fmt" + + "go.uber.org/zap" + + "github.com/pipe-cd/pipe/pkg/config" + "github.com/pipe-cd/pipe/pkg/model" +) + +type Builder interface { + Build(ctx context.Context, id string, cmd model.Command_BuildPlanPreview) ([]*model.ApplicationPlanPreviewResult, error) +} + +type builder struct { + gitClient gitClient + applicationLister applicationLister + config *config.PipedSpec + logger *zap.Logger +} + +func newBuilder(gc gitClient, al applicationLister, cfg *config.PipedSpec, logger *zap.Logger) *builder { + return &builder{ + gitClient: gc, + applicationLister: al, + config: cfg, + logger: logger.Named("planpreview-builder"), + } +} + +func (b *builder) Build(ctx context.Context, id string, cmd model.Command_BuildPlanPreview) ([]*model.ApplicationPlanPreviewResult, error) { + repoCfg, ok := b.config.GetRepository(cmd.RepositoryId) + if !ok { + return nil, fmt.Errorf("repository %s was not found in Piped config", cmd.RepositoryId) + } + if repoCfg.Branch != cmd.BaseBranch { + return nil, fmt.Errorf("base branch repository %s was not correct, requested %s, expected %s", cmd.RepositoryId, cmd.BaseBranch, repoCfg.Branch) + } + + apps := b.listApplications(repoCfg) + if len(apps) == 0 { + return nil, nil + } + + repo, err := b.gitClient.Clone(ctx, repoCfg.RepoID, repoCfg.Remote, repoCfg.Branch, "") + if err != nil { + return nil, fmt.Errorf("failed to clone git repository %s", cmd.RepositoryId) + } + defer repo.Clean() + + // TODO: Implement planpreview builder. + // 1. Fetch the source code at the head commit. + // 2. Determine the list of applications that will be triggered. + // - Based on the changed files between 2 commits: head commit and mostRecentlyTriggeredCommit + // 3. For each application: + // 3.1. Start a builder to check what/why strategy will be used + // 3.2. Check what resources should be added, deleted and modified + // - Terraform app: used terraform plan command + // - Kubernetes app: calculate the diff of resources at head commit and mostRecentlySuccessfulCommit + return nil, fmt.Errorf("Not Implemented") +} + +func (b *builder) listApplications(repo config.PipedRepository) []*model.Application { + apps := b.applicationLister.List() + out := make([]*model.Application, 0, len(apps)) + + for _, app := range apps { + if app.GitPath.Repo.Id != repo.RepoID { + continue + } + if app.GitPath.Repo.Remote != repo.Remote { + continue + } + if app.GitPath.Repo.Branch != repo.Branch { + continue + } + out = append(out, app) + } + + return out +} diff --git a/pkg/app/piped/planpreview/builder_test.go b/pkg/app/piped/planpreview/builder_test.go new file mode 100644 index 0000000000..471c861fcb --- /dev/null +++ b/pkg/app/piped/planpreview/builder_test.go @@ -0,0 +1,15 @@ +// Copyright 2021 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 planpreview diff --git a/pkg/app/piped/planpreview/handler.go b/pkg/app/piped/planpreview/handler.go new file mode 100644 index 0000000000..78d102c3a3 --- /dev/null +++ b/pkg/app/piped/planpreview/handler.go @@ -0,0 +1,235 @@ +// Copyright 2021 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 planpreview + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "go.uber.org/zap" + + "github.com/pipe-cd/pipe/pkg/config" + "github.com/pipe-cd/pipe/pkg/git" + "github.com/pipe-cd/pipe/pkg/model" +) + +const ( + defaultWorkerNum = 3 + defaultCommandQueueBufferSize = 10 + defaultCommandCheckInterval = 5 * time.Second + defaultCommandHandleTimeout = 5 * time.Minute +) + +type options struct { + workerNum int + commandQueueBufferSize int + commandCheckInterval time.Duration + commandHandleTimeout time.Duration + logger *zap.Logger +} + +type Option func(*options) + +func WithWorkerNum(n int) Option { + return func(opts *options) { + opts.workerNum = n + } +} + +func WithCommandQueueBufferSize(s int) Option { + return func(opts *options) { + opts.commandQueueBufferSize = s + } +} + +func WithCommandCheckInterval(i time.Duration) Option { + return func(opts *options) { + opts.commandCheckInterval = i + } +} + +func WithCommandHandleTimeout(t time.Duration) Option { + return func(opts *options) { + opts.commandHandleTimeout = t + } +} + +func WithLogger(l *zap.Logger) Option { + return func(opts *options) { + opts.logger = l + } +} + +type gitClient interface { + Clone(ctx context.Context, repoID, remote, branch, destination string) (git.Repo, error) +} + +type applicationLister interface { + List() []*model.Application +} + +type commandLister interface { + ListBuildPlanPreviewCommands() []model.ReportableCommand +} + +type Handler struct { + gitClient gitClient + commandLister commandLister + + commandCh chan model.ReportableCommand + prevCommands map[string]struct{} + + options *options + builderFactory func() Builder + logger *zap.Logger +} + +func NewHandler(gc gitClient, cl commandLister, al applicationLister, cfg *config.PipedSpec, opts ...Option) *Handler { + opt := &options{ + workerNum: defaultWorkerNum, + commandQueueBufferSize: defaultCommandQueueBufferSize, + commandCheckInterval: defaultCommandCheckInterval, + commandHandleTimeout: defaultCommandHandleTimeout, + logger: zap.NewNop(), + } + for _, o := range opts { + o(opt) + } + h := &Handler{ + gitClient: gc, + commandLister: cl, + commandCh: make(chan model.ReportableCommand, opt.commandQueueBufferSize), + prevCommands: map[string]struct{}{}, + options: opt, + logger: opt.logger.Named("planpreview-handler"), + } + h.builderFactory = func() Builder { + return newBuilder(gc, al, cfg, h.logger) + } + + return h +} + +// Run starts running Handler until the given context has done. +func (h *Handler) Run(ctx context.Context) error { + h.logger.Info("start running planpreview handler") + + startWorker := func(ctx context.Context, cmdCh <-chan model.ReportableCommand) { + for { + select { + case cmd := <-cmdCh: + ctx, _ = context.WithTimeout(ctx, h.options.commandHandleTimeout) + h.handleCommand(ctx, cmd) + + case <-ctx.Done(): + return + } + } + } + + h.logger.Info(fmt.Sprintf("spawn %d worker to handle commands", h.options.workerNum)) + for i := 0; i < h.options.workerNum; i++ { + go startWorker(ctx, h.commandCh) + } + + commandTicker := time.NewTicker(h.options.commandCheckInterval) + defer commandTicker.Stop() + +L: + for { + select { + case <-ctx.Done(): + break L + + case <-commandTicker.C: + h.enqueueNewCommands(ctx) + } + } + + h.logger.Info("planpreview handler has been stopped") + return nil +} + +func (h *Handler) enqueueNewCommands(ctx context.Context) { + h.logger.Info("fetching unhandled commands to enqueue") + + commands := h.commandLister.ListBuildPlanPreviewCommands() + if len(commands) == 0 { + h.logger.Info("there is no new command to enqueue") + return + } + + news := make([]model.ReportableCommand, 0, len(commands)) + prevCommands := make(map[string]struct{}, len(commands)) + for _, cmd := range commands { + prevCommands[cmd.Id] = struct{}{} + if _, ok := h.prevCommands[cmd.Id]; !ok { + news = append(news, cmd) + } + } + + h.prevCommands = prevCommands + h.logger.Info(fmt.Sprintf("will enqueue %d new commands", len(news))) + + for _, cmd := range news { + select { + case h.commandCh <- cmd: + case <-ctx.Done(): + return + } + } +} + +func (h *Handler) handleCommand(ctx context.Context, cmd model.ReportableCommand) { + result := &model.PlanPreviewCommandResult{ + CommandId: cmd.Id, + } + + reportError := func(err error) { + result.Error = err.Error() + output, err := json.Marshal(result) + if err != nil { + h.logger.Error("failed to marshal command result", zap.Error(err)) + } + if err := cmd.Report(ctx, model.CommandStatus_COMMAND_FAILED, nil, output); err != nil { + h.logger.Error("failed to report command status", zap.Error(err)) + } + } + + if cmd.BuildPlanPreview == nil { + reportError(fmt.Errorf("malformed command")) + return + } + + b := h.builderFactory() + appResults, err := b.Build(ctx, cmd.Id, *cmd.BuildPlanPreview) + if err != nil { + reportError(err) + return + } + + result.Results = appResults + output, err := json.Marshal(result) + if err != nil { + reportError(fmt.Errorf("failed to marshal command result (%w)", err)) + return + } + + if err := cmd.Report(ctx, model.CommandStatus_COMMAND_SUCCEEDED, nil, output); err != nil { + h.logger.Error("failed to report command status", zap.Error(err)) + } +} diff --git a/pkg/app/piped/planpreview/handler_test.go b/pkg/app/piped/planpreview/handler_test.go new file mode 100644 index 0000000000..86417ccd0e --- /dev/null +++ b/pkg/app/piped/planpreview/handler_test.go @@ -0,0 +1,130 @@ +// Copyright 2021 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 planpreview + +import ( + "context" + "sort" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/pipe-cd/pipe/pkg/model" +) + +type testCommandLister struct { + commands []model.Command +} + +func (l *testCommandLister) ListBuildPlanPreviewCommands() []model.ReportableCommand { + out := make([]model.ReportableCommand, 0, len(l.commands)) + for i := range l.commands { + out = append(out, model.ReportableCommand{ + Command: &l.commands[i], + Report: func(ctx context.Context, status model.CommandStatus, metadata map[string]string, output []byte) error { + return nil + }, + }) + } + return out +} + +type testBuilder struct { + recorder func(id string) +} + +func (b *testBuilder) Build(ctx context.Context, id string, cmd model.Command_BuildPlanPreview) ([]*model.ApplicationPlanPreviewResult, error) { + b.recorder(id) + return nil, nil +} + +func TestHandler(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cl := &testCommandLister{} + handledCommands := make([]string, 0) + var mu sync.Mutex + var wg sync.WaitGroup + + handler := NewHandler(nil, cl, nil, nil, + WithWorkerNum(2), + // Use a long interval because we will directly call enqueueNewCommands function in this test. + WithCommandCheckInterval(time.Hour), + ) + handler.builderFactory = func() Builder { + return &testBuilder{ + recorder: func(id string) { + defer wg.Done() + mu.Lock() + defer mu.Unlock() + handledCommands = append(handledCommands, id) + sort.Strings(handledCommands) + }, + } + } + go handler.Run(ctx) + + // CommandLister returns no command, + // then there is no new command. + handler.enqueueNewCommands(ctx) + + require.Equal(t, []string{}, handledCommands) + + // CommandLister returns 2 commands: 1, 2. + // both of them will be considered as new commands. + wg.Add(2) + cl.commands = []model.Command{ + { + Id: "1", + Type: model.Command_BUILD_PLAN_PREVIEW, + BuildPlanPreview: &model.Command_BuildPlanPreview{}, + }, + { + Id: "2", + Type: model.Command_BUILD_PLAN_PREVIEW, + BuildPlanPreview: &model.Command_BuildPlanPreview{}, + }, + } + handler.enqueueNewCommands(ctx) + wg.Wait() + require.Equal(t, []string{"1", "2"}, handledCommands) + + // CommandLister returns the same command list + // so no new command will be added. + handler.enqueueNewCommands(ctx) + require.Equal(t, []string{"1", "2"}, handledCommands) + + // CommandLister returns commands: 2, 3. + // then 3 will be considered as a new command. + wg.Add(1) + cl.commands = []model.Command{ + { + Id: "2", + Type: model.Command_BUILD_PLAN_PREVIEW, + BuildPlanPreview: &model.Command_BuildPlanPreview{}, + }, + { + Id: "3", + Type: model.Command_BUILD_PLAN_PREVIEW, + BuildPlanPreview: &model.Command_BuildPlanPreview{}, + }, + } + handler.enqueueNewCommands(ctx) + wg.Wait() + require.Equal(t, []string{"1", "2", "3"}, handledCommands) +} diff --git a/pkg/app/piped/planpreview/planpreview.go b/pkg/app/piped/planpreview/planpreview.go deleted file mode 100644 index f7239d6a8e..0000000000 --- a/pkg/app/piped/planpreview/planpreview.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2021 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 planpreview - -import ( - "context" - - "github.com/pipe-cd/pipe/pkg/model" -) - -type Planner interface { - Plan(ctx context.Context, repoID, branch, commit string) ([]*model.ApplicationPlanPreviewResult, error) -} - -type planner struct { -} - -func (p *planner) Plan(ctx context.Context, repoID, branch, commit string) ([]*model.ApplicationPlanPreviewResult, error) { - // TODO: Implement Plan functionality. - - // 1. List all applications placing in that repository. - // 2. Fetch the source code at the specified branch commit. - // 3. Determine the list of applications that will be triggered. - // - Based on the changed files between 2 commits: head commit and mostRecentlyTriggeredCommit - // 4. For each application: - // 4.1. Start a planner to check what/why strategy will be used - // 4.2. Check what resources should be added, deleted and modified - // - Terraform app: used terraform plan command - // - Kubernetes app: calculate the diff of resources at head commit and mostRecentlySuccessfulCommit - - return nil, nil -} diff --git a/pkg/model/command.proto b/pkg/model/command.proto index 7562d68b66..25c7a530f3 100644 --- a/pkg/model/command.proto +++ b/pkg/model/command.proto @@ -78,6 +78,7 @@ message Command { CommandStatus status = 20; map metadata = 21; int64 handled_at = 22 [(validate.rules).int64.gte = 0]; + // TODO: Add a new field to show why command was failed. Type type = 30 [(validate.rules).enum.defined_only = true]; SyncApplication sync_application = 31; diff --git a/pkg/model/planpreview.proto b/pkg/model/planpreview.proto index 1867c2c20f..07109ec2ff 100644 --- a/pkg/model/planpreview.proto +++ b/pkg/model/planpreview.proto @@ -21,6 +21,12 @@ option go_package = "github.com/pipe-cd/pipe/pkg/model"; import "validate/validate.proto"; import "pkg/model/common.proto"; +message PlanPreviewCommandResult { + string command_id = 1 [(validate.rules).string.min_len = 1]; + repeated ApplicationPlanPreviewResult results = 2; + string error = 3; +} + message ApplicationPlanPreviewResult { // Application information. string application_id = 1;