diff --git a/BUILD.bazel b/BUILD.bazel index c94cdd13d6..626b247ce7 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -63,6 +63,7 @@ genrule( # gazelle:exclude pkg/model/notificationevent.pb.validate.go # gazelle:exclude pkg/model/piped.pb.validate.go # gazelle:exclude pkg/model/piped_stats.pb.validate.go +# gazelle:exclude pkg/model/planpreview.pb.validate.go # gazelle:exclude pkg/model/project.pb.validate.go # gazelle:exclude pkg/model/role.pb.validate.go # gazelle:exclude pkg/model/user.pb.validate.go diff --git a/cmd/pipecd/server.go b/cmd/pipecd/server.go index 3d4d9ca13b..e38ec3b6f1 100644 --- a/cmd/pipecd/server.go +++ b/cmd/pipecd/server.go @@ -222,7 +222,7 @@ func (s *server) run(ctx context.Context, t cli.Telemetry) error { datastore.NewAPIKeyStore(ds), t.Logger, ) - service = grpcapi.NewAPI(ds, cmds, cmdOutputStore, t.Logger) + service = grpcapi.NewAPI(ds, cmds, cmdOutputStore, cfg.Address, t.Logger) opts = []rpc.Option{ rpc.WithPort(s.apiPort), rpc.WithGracePeriod(s.gracePeriod), diff --git a/pkg/app/api/grpcapi/api.go b/pkg/app/api/grpcapi/api.go index 3fc15d5d91..36a08e4754 100644 --- a/pkg/app/api/grpcapi/api.go +++ b/pkg/app/api/grpcapi/api.go @@ -44,7 +44,8 @@ type API struct { commandStore commandstore.Store commandOutputGetter commandOutputGetter - logger *zap.Logger + webBaseURL string + logger *zap.Logger } // NewAPI creates a new API instance. @@ -52,6 +53,7 @@ func NewAPI( ds datastore.DataStore, cmds commandstore.Store, cog commandOutputGetter, + webBaseURL string, logger *zap.Logger, ) *API { a := &API{ @@ -62,6 +64,7 @@ func NewAPI( eventStore: datastore.NewEventStore(ds), commandStore: cmds, commandOutputGetter: cog, + webBaseURL: webBaseURL, logger: logger.Named("api"), } return a @@ -437,7 +440,20 @@ func (a *API) GetPlanPreviewResults(ctx context.Context, req *apiservice.GetPlan return nil, err } - const freshDuration = 24 * time.Hour + const ( + freshDuration = 24 * time.Hour + defaultCommandHandleTimeout = 5 * time.Minute + ) + + var ( + handledCommands = make([]string, 0, len(req.Commands)) + results = make([]*model.PlanPreviewCommandResult, 0, len(req.Commands)) + ) + + commandHandleTimeout := time.Duration(req.CommandHandleTimeout) * time.Second + if commandHandleTimeout == 0 { + commandHandleTimeout = defaultCommandHandleTimeout + } // Validate based on command model stored in datastore. for _, commandID := range req.Commands { @@ -456,24 +472,35 @@ func (a *API) GetPlanPreviewResults(ctx context.Context, req *apiservice.GetPlan if cmd.Type != model.Command_BUILD_PLAN_PREVIEW { return nil, status.Error(codes.FailedPrecondition, fmt.Sprint("Command %s is not a plan preview command", commandID)) } + if !cmd.IsHandled() { - return nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("Command %s is not completed yet", commandID)) + if time.Since(time.Unix(cmd.CreatedAt, 0)) <= commandHandleTimeout { + return nil, status.Error(codes.NotFound, fmt.Sprintf("No command ouput for command %d because it is not completed yet", commandID)) + } + results = append(results, &model.PlanPreviewCommandResult{ + CommandId: cmd.Id, + PipedId: cmd.PipedId, + Error: fmt.Sprintf("Timed out, maybe the Piped is offline currently."), + }) + continue } + // There is no reason to fetch output data of command that has been completed a long time ago. // So in order to prevent unintended actions, we disallow that ability. if time.Since(time.Unix(cmd.HandledAt, 0)) > freshDuration { return nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("The output data for command %s is too old for access", commandID)) } - } - results := make([]*model.PlanPreviewCommandResult, 0, len(req.Commands)) + handledCommands = append(handledCommands, commandID) + } // Fetch ouput data to build results. - for _, commandID := range req.Commands { + for _, commandID := range handledCommands { data, err := a.commandOutputGetter.Get(ctx, commandID) if err != nil { return nil, status.Error(codes.Internal, fmt.Sprintf("Failed to retrieve output data of command %s", commandID)) } + var result model.PlanPreviewCommandResult if err := json.Unmarshal(data, &result); err != nil { a.logger.Error("failed to unmarshal planpreview command result", @@ -482,9 +509,16 @@ func (a *API) GetPlanPreviewResults(ctx context.Context, req *apiservice.GetPlan ) return nil, status.Error(codes.Internal, fmt.Sprintf("Failed to decode output data of command %s", commandID)) } + results = append(results, &result) } + // All URL fields inside the result model are empty. + // So we fill them before sending to the client. + for _, r := range results { + r.FillURLs(a.webBaseURL) + } + return &apiservice.GetPlanPreviewResultsResponse{ Results: results, }, nil diff --git a/pkg/app/api/service/apiservice/service.proto b/pkg/app/api/service/apiservice/service.proto index 5c1a306d43..266fb5157d 100644 --- a/pkg/app/api/service/apiservice/service.proto +++ b/pkg/app/api/service/apiservice/service.proto @@ -123,6 +123,8 @@ message RequestPlanPreviewResponse { message GetPlanPreviewResultsRequest { repeated string commands = 1; + // Maximum number of seconds a Piped can take to handle a command. + int64 command_handle_timeout = 2; } message GetPlanPreviewResultsResponse { diff --git a/pkg/app/pipectl/cmd/planpreview/BUILD.bazel b/pkg/app/pipectl/cmd/planpreview/BUILD.bazel index e3aa8c71fb..cc0ab8c964 100644 --- a/pkg/app/pipectl/cmd/planpreview/BUILD.bazel +++ b/pkg/app/pipectl/cmd/planpreview/BUILD.bazel @@ -1,4 +1,4 @@ -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", @@ -15,3 +15,14 @@ go_library( "@org_golang_google_grpc//status:go_default_library", ], ) + +go_test( + name = "go_default_test", + size = "small", + srcs = ["planpreview_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/model:go_default_library", + "@com_github_stretchr_testify//assert:go_default_library", + ], +) diff --git a/pkg/app/pipectl/cmd/planpreview/planpreview.go b/pkg/app/pipectl/cmd/planpreview/planpreview.go index 72aaa16c1c..ade52140ba 100644 --- a/pkg/app/pipectl/cmd/planpreview/planpreview.go +++ b/pkg/app/pipectl/cmd/planpreview/planpreview.go @@ -16,7 +16,12 @@ package planpreview import ( "context" + "encoding/json" "fmt" + "io" + "io/ioutil" + "os" + "strings" "time" "github.com/spf13/cobra" @@ -29,23 +34,31 @@ import ( "github.com/pipe-cd/pipe/pkg/model" ) +const ( + defaultTimeout = 10 * time.Minute + defaultPipedHandleTimeout = 5 * time.Minute + defaultCheckInterval = 10 * time.Second +) + type command struct { - repoRemoteURL string - headBranch string - headCommit string - baseBranch string - out string - timeout time.Duration - checkInterval time.Duration + repoRemoteURL string + headBranch string + headCommit string + baseBranch string + out string + timeout time.Duration + pipedHandleTimeout time.Duration + checkInterval time.Duration clientOptions *client.Options } func NewCommand() *cobra.Command { c := &command{ - clientOptions: &client.Options{}, - timeout: 10 * time.Minute, - checkInterval: 10 * time.Second, + clientOptions: &client.Options{}, + pipedHandleTimeout: defaultPipedHandleTimeout, + timeout: defaultTimeout, + checkInterval: defaultCheckInterval, } cmd := &cobra.Command{ Use: "plan-preview", @@ -60,12 +73,13 @@ func NewCommand() *cobra.Command { 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.Flags().DurationVar(&c.timeout, "timeout", c.timeout, "Maximum amount of time this command has to complete. Default is 10m.") + cmd.Flags().DurationVar(&c.pipedHandleTimeout, "piped-handle-timeout", c.pipedHandleTimeout, "Maximum amount of time that a Piped can take to handle. Default is 5m.") cmd.MarkFlagRequired("repo-remote-url") cmd.MarkFlagRequired("head-branch") cmd.MarkFlagRequired("head-commit") cmd.MarkFlagRequired("base-branch") - cmd.MarkFlagRequired("out") return cmd } @@ -89,18 +103,23 @@ func (c *command) run(ctx context.Context, _ cli.Telemetry) error { resp, err := cli.RequestPlanPreview(ctx, req) if err != nil { - fmt.Printf("Failed to request plan preview: %v\n", err) + fmt.Printf("Failed to request plan-preview: %v\n", err) return err } + if len(resp.Commands) == 0 { + fmt.Println("There is no piped that is handling the given Git repository") + return nil + } + fmt.Printf("Requested plan-preview, waiting for its results (commands: %v)\n", resp.Commands) getResults := func(commands []string) ([]*model.PlanPreviewCommandResult, error) { req := &apiservice.GetPlanPreviewResultsRequest{ - Commands: commands, + Commands: commands, + CommandHandleTimeout: int64(c.pipedHandleTimeout.Seconds()), } resp, err := cli.GetPlanPreviewResults(ctx, req) if err != nil { - fmt.Printf("Failed to get plan preview results: %v", err) return nil, err } @@ -119,19 +138,162 @@ func (c *command) run(ctx context.Context, _ cli.Telemetry) error { results, err := getResults(resp.Commands) if err != nil { if status.Code(err) == codes.NotFound { + fmt.Println("waiting...") break } + fmt.Printf("Failed to retrieve plan-preview results: %v\n", err) return err } - return c.printResults(results) + return printResults(results, os.Stdout, c.out) } } return nil } -func (c *command) printResults(results []*model.PlanPreviewCommandResult) error { - // TODO: Format preview results and support writing the result into file. - fmt.Println(results) - return nil +func printResults(results []*model.PlanPreviewCommandResult, stdout io.Writer, outFile string) error { + r := convert(results) + + // Print out a readable format to stdout. + fmt.Fprint(stdout, r) + + if outFile == "" { + return nil + } + + // Write JSON format to the given file. + data, err := json.Marshal(r) + if err != nil { + fmt.Printf("Failed to encode result to JSON: %v\n", err) + return err + } + return ioutil.WriteFile(outFile, data, 0644) +} + +func convert(results []*model.PlanPreviewCommandResult) ReadableResult { + out := ReadableResult{} + for _, r := range results { + if r.Error != "" { + out.FailurePipeds = append(out.FailurePipeds, FailurePiped{ + PipedInfo: PipedInfo{ + PipedID: r.PipedId, + PipedURL: r.PipedUrl, + }, + Reason: r.Error, + }) + continue + } + + for _, a := range r.Results { + appInfo := ApplicationInfo{ + ApplicationID: a.ApplicationId, + ApplicationName: a.ApplicationName, + ApplicationURL: a.ApplicationUrl, + ApplicationKind: a.ApplicationKind.String(), + ApplicationDirectory: a.ApplicationDirectory, + EnvID: a.EnvId, + EnvName: a.EnvName, + EnvURL: a.EnvUrl, + } + if a.Error != "" { + out.FailureApplications = append(out.FailureApplications, FailureApplication{ + ApplicationInfo: appInfo, + Reason: a.Error, + }) + continue + } + out.Applications = append(out.Applications, ApplicationResult{ + ApplicationInfo: appInfo, + SyncStrategy: a.SyncStrategy.String(), + Changes: string(a.Changes), + }) + } + } + + return out +} + +type ReadableResult struct { + Applications []ApplicationResult + FailureApplications []FailureApplication + FailurePipeds []FailurePiped +} + +type ApplicationResult struct { + ApplicationInfo + SyncStrategy string // QUICK_SYNC, PIPELINE + Changes string +} + +type FailurePiped struct { + PipedInfo + Reason string +} + +type FailureApplication struct { + ApplicationInfo + Reason string +} + +type PipedInfo struct { + PipedID string + PipedURL string +} + +type ApplicationInfo struct { + ApplicationID string + ApplicationName string + ApplicationURL string + EnvID string + EnvName string + EnvURL string + ApplicationKind string // KUBERNETES, TERRAFORM, CLOUDRUN, LAMBDA, ECS + ApplicationDirectory string +} + +func (r ReadableResult) String() string { + var b strings.Builder + if len(r.Applications)+len(r.FailureApplications)+len(r.FailurePipeds) == 0 { + fmt.Fprintf(&b, "\nThere are no applications to build plan-preview\n") + return b.String() + } + + if len(r.Applications) > 0 { + if len(r.Applications) > 1 { + fmt.Fprintf(&b, "\nHere are plan-preview for %d applications:\n", len(r.Applications)) + } else { + fmt.Fprintf(&b, "\nHere are plan-preview for 1 application:\n") + } + for i, app := range r.Applications { + fmt.Fprintf(&b, "\n%d. app: %s, env: %s, kind: %s\n", i+1, app.ApplicationName, app.EnvName, app.ApplicationKind) + fmt.Fprintf(&b, " sync strategy: %s\n", app.SyncStrategy) + fmt.Fprintf(&b, " changes: %s\n", app.Changes) + } + } + + if len(r.FailureApplications) > 0 { + if len(r.FailureApplications) > 1 { + fmt.Fprintf(&b, "\nNOTE: An error occurred while building plan-preview for the following %d applications:\n", len(r.FailureApplications)) + } else { + fmt.Fprintf(&b, "\nNOTE: An error occurred while building plan-preview for the following application:\n") + } + for i, app := range r.FailureApplications { + fmt.Fprintf(&b, "\n%d. app: %s, env: %s, kind: %s\n", i+1, app.ApplicationName, app.EnvName, app.ApplicationKind) + fmt.Fprintf(&b, " reason: %s\n", app.Reason) + } + } + + if len(r.FailurePipeds) > 0 { + if len(r.FailurePipeds) > 1 { + fmt.Fprintf(&b, "\nNOTE: An error occurred while building plan-preview for applications of the following %d Pipeds:\n", len(r.FailurePipeds)) + } else { + fmt.Fprintf(&b, "\nNOTE: An error occurred while building plan-preview for applications of the following Piped:\n") + } + for i, piped := range r.FailurePipeds { + fmt.Fprintf(&b, "\n%d. piped: %s\n", i+1, piped.PipedID) + fmt.Fprintf(&b, " reason: %s\n", piped.Reason) + } + } + + return b.String() } diff --git a/pkg/app/pipectl/cmd/planpreview/planpreview_test.go b/pkg/app/pipectl/cmd/planpreview/planpreview_test.go new file mode 100644 index 0000000000..052055da5d --- /dev/null +++ b/pkg/app/pipectl/cmd/planpreview/planpreview_test.go @@ -0,0 +1,205 @@ +// 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 ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/pipe-cd/pipe/pkg/model" +) + +func TestReadableResultString(t *testing.T) { + testcases := []struct { + name string + results []*model.PlanPreviewCommandResult + expected string + }{ + { + name: "empty", + results: []*model.PlanPreviewCommandResult{}, + expected: ` +There are no applications to build plan-preview +`, + }, + { + name: "there is only a plannable application", + results: []*model.PlanPreviewCommandResult{ + { + CommandId: "command-2", + PipedId: "piped-2", + PipedUrl: "https://pipecd.dev/piped-2", + Results: []*model.ApplicationPlanPreviewResult{ + { + ApplicationId: "app-1", + ApplicationName: "app-1", + ApplicationUrl: "https://pipecd.dev/app-1", + ApplicationKind: model.ApplicationKind_KUBERNETES, + EnvName: "env-1", + SyncStrategy: model.SyncStrategy_QUICK_SYNC, + Changes: []byte("changes-1"), + }, + }, + }, + }, + expected: ` +Here are plan-preview for 1 application: + +1. app: app-1, env: env-1, kind: KUBERNETES + sync strategy: QUICK_SYNC + changes: changes-1 +`, + }, + { + name: "there is only a failure application", + results: []*model.PlanPreviewCommandResult{ + { + CommandId: "command-2", + PipedId: "piped-2", + PipedUrl: "https://pipecd.dev/piped-2", + Results: []*model.ApplicationPlanPreviewResult{ + { + ApplicationId: "app-2", + ApplicationName: "app-2", + ApplicationUrl: "https://pipecd.dev/app-2", + ApplicationKind: model.ApplicationKind_TERRAFORM, + EnvName: "env-2", + Error: "wrong application configuration", + }, + }, + }, + }, + expected: ` +NOTE: An error occurred while building plan-preview for the following application: + +1. app: app-2, env: env-2, kind: TERRAFORM + reason: wrong application configuration +`, + }, + { + name: "there is only a failure piped", + results: []*model.PlanPreviewCommandResult{ + { + CommandId: "command-1", + PipedId: "piped-1", + PipedUrl: "https://pipecd.dev/piped-1", + Error: "failed to clone", + }, + }, + expected: ` +NOTE: An error occurred while building plan-preview for applications of the following Piped: + +1. piped: piped-1 + reason: failed to clone +`, + }, + { + name: "all kinds", + results: []*model.PlanPreviewCommandResult{ + { + CommandId: "command-1", + PipedId: "piped-1", + PipedUrl: "https://pipecd.dev/piped-1", + Error: "failed to clone", + }, + { + CommandId: "command-2", + PipedId: "piped-2", + PipedUrl: "https://pipecd.dev/piped-2", + Results: []*model.ApplicationPlanPreviewResult{ + { + ApplicationId: "app-1", + ApplicationName: "app-1", + ApplicationUrl: "https://pipecd.dev/app-1", + ApplicationKind: model.ApplicationKind_KUBERNETES, + EnvName: "env-1", + SyncStrategy: model.SyncStrategy_QUICK_SYNC, + Changes: []byte("changes-1"), + }, + { + ApplicationId: "app-2", + ApplicationName: "app-2", + ApplicationUrl: "https://pipecd.dev/app-2", + ApplicationKind: model.ApplicationKind_TERRAFORM, + EnvName: "env-2", + SyncStrategy: model.SyncStrategy_PIPELINE, + Changes: []byte("changes-2"), + }, + { + ApplicationId: "app-3", + ApplicationName: "app-3", + ApplicationUrl: "https://pipecd.dev/app-3", + ApplicationKind: model.ApplicationKind_TERRAFORM, + EnvName: "env-3", + Error: "wrong application configuration", + }, + { + ApplicationId: "app-4", + ApplicationName: "app-4", + ApplicationUrl: "https://pipecd.dev/app-4", + ApplicationKind: model.ApplicationKind_CLOUDRUN, + EnvName: "env-4", + Error: "missing key", + }, + }, + }, + { + CommandId: "command-3", + PipedId: "piped-3", + PipedUrl: "https://pipecd.dev/piped-3", + Error: "failed to checkout branch", + }, + }, + expected: ` +Here are plan-preview for 2 applications: + +1. app: app-1, env: env-1, kind: KUBERNETES + sync strategy: QUICK_SYNC + changes: changes-1 + +2. app: app-2, env: env-2, kind: TERRAFORM + sync strategy: PIPELINE + changes: changes-2 + +NOTE: An error occurred while building plan-preview for the following 2 applications: + +1. app: app-3, env: env-3, kind: TERRAFORM + reason: wrong application configuration + +2. app: app-4, env: env-4, kind: CLOUDRUN + reason: missing key + +NOTE: An error occurred while building plan-preview for applications of the following 2 Pipeds: + +1. piped: piped-1 + reason: failed to clone + +2. piped: piped-3 + reason: failed to checkout branch +`, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + var buf bytes.Buffer + printResults(tc.results, &buf, "") + + assert.Equal(t, tc.expected, buf.String()) + }) + } +} diff --git a/pkg/app/piped/planpreview/builder.go b/pkg/app/piped/planpreview/builder.go index dbb6c49267..7ccca7c1cc 100644 --- a/pkg/app/piped/planpreview/builder.go +++ b/pkg/app/piped/planpreview/builder.go @@ -45,6 +45,8 @@ func newBuilder(gc gitClient, al applicationLister, cfg *config.PipedSpec, logge } func (b *builder) Build(ctx context.Context, id string, cmd model.Command_BuildPlanPreview) ([]*model.ApplicationPlanPreviewResult, error) { + b.logger.Info(fmt.Sprintf("start building planpreview result for command %s", id)) + repoCfg, ok := b.config.GetRepository(cmd.RepositoryId) if !ok { return nil, fmt.Errorf("repository %s was not found in Piped config", cmd.RepositoryId) diff --git a/pkg/model/BUILD.bazel b/pkg/model/BUILD.bazel index 0d94d63712..715d38287d 100644 --- a/pkg/model/BUILD.bazel +++ b/pkg/model/BUILD.bazel @@ -59,6 +59,7 @@ go_library( "model.go", "notificationevent.go", "piped.go", + "planpreview.go", "project.go", "stage.go", ], diff --git a/pkg/model/application.go b/pkg/model/application.go index abe9b0837a..8b29c02255 100644 --- a/pkg/model/application.go +++ b/pkg/model/application.go @@ -15,6 +15,7 @@ package model import ( + "path" "path/filepath" ) @@ -43,3 +44,7 @@ func (s ApplicationSyncState) HasChanged(next ApplicationSyncState) bool { } return false } + +func MakeApplicationURL(baseURL, applicationID string) string { + return path.Join(baseURL, "applications", applicationID) +} diff --git a/pkg/model/environment.go b/pkg/model/environment.go index e47d2e9c3d..71d397d8f0 100644 --- a/pkg/model/environment.go +++ b/pkg/model/environment.go @@ -13,3 +13,9 @@ // limitations under the License. package model + +import "path" + +func MakeEnvironmentURL(baseURL, environmentID string) string { + return path.Join(baseURL, "settings", "environment") +} diff --git a/pkg/model/piped.go b/pkg/model/piped.go index 04ae883f5c..6ddba082a1 100644 --- a/pkg/model/piped.go +++ b/pkg/model/piped.go @@ -17,6 +17,7 @@ package model import ( "errors" "fmt" + "path" "time" "golang.org/x/crypto/bcrypt" @@ -137,3 +138,7 @@ func (p *Piped) RedactSensitiveData() { p.Keys[i].Hash = redactedMessage } } + +func MakePipedURL(baseURL, pipedID string) string { + return path.Join(baseURL, "settings", "piped") +} diff --git a/pkg/model/planpreview.go b/pkg/model/planpreview.go new file mode 100644 index 0000000000..eee7daad81 --- /dev/null +++ b/pkg/model/planpreview.go @@ -0,0 +1,23 @@ +// 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 model + +func (r *PlanPreviewCommandResult) FillURLs(baseURL string) { + r.PipedUrl = MakePipedURL(baseURL, r.PipedId) + for _, ar := range r.Results { + ar.ApplicationUrl = MakeApplicationURL(baseURL, ar.ApplicationId) + ar.EnvUrl = MakeEnvironmentURL(baseURL, ar.EnvId) + } +} diff --git a/pkg/model/planpreview.proto b/pkg/model/planpreview.proto index 07109ec2ff..f48ef6ed30 100644 --- a/pkg/model/planpreview.proto +++ b/pkg/model/planpreview.proto @@ -23,25 +23,46 @@ import "pkg/model/common.proto"; message PlanPreviewCommandResult { string command_id = 1 [(validate.rules).string.min_len = 1]; - repeated ApplicationPlanPreviewResult results = 2; - string error = 3; + // The Piped that handles command. + string piped_id = 2 [(validate.rules).string.min_len = 1]; + // Web URL to the piped page. + // This is only filled before returning to the client. + string piped_url = 3; + + repeated ApplicationPlanPreviewResult results = 4; + // Error while handling command. + string error = 5; } message ApplicationPlanPreviewResult { // Application information. - string application_id = 1; - string env_id = 2; - string piped_id = 3; - string project_id = 4; - ApplicationKind kind = 6 [(validate.rules).enum.defined_only = true]; + string application_id = 1 [(validate.rules).string.min_len = 1]; + string application_name = 2 [(validate.rules).string.min_len = 1]; + // Web URL to the application page. + // This is only filled before returning to the client. + string application_url = 3; + ApplicationKind application_kind = 4 [(validate.rules).enum.defined_only = true]; + string application_directory = 5 [(validate.rules).string.min_len = 1]; + + string env_id = 6 [(validate.rules).string.min_len = 1]; + string env_name = 7 [(validate.rules).string.min_len = 1]; + // Web URL to the environment page. + // This is only filled before returning to the client. + string env_url = 8; + + string piped_id = 9 [(validate.rules).string.min_len = 1]; + string project_id = 10 [(validate.rules).string.min_len = 1]; // Target commit information. - string target_branch = 10; - string target_head_commit = 11; + string target_branch = 20 [(validate.rules).string.min_len = 1]; + string target_head_commit = 21 [(validate.rules).string.min_len = 1]; + + // Planpreview result. + SyncStrategy sync_strategy = 30; + bytes changes = 31; - // Plan preview result. - SyncStrategy sync_strategy = 13; - bytes changes = 14; + // Error while building planpreview result. + string error = 40; - int64 created_at = 15; + int64 created_at = 90 [(validate.rules).int64.gt = 0]; }