diff --git a/internal/services/gateway/action/commitstatusdelivery.go b/internal/services/gateway/action/commitstatusdelivery.go new file mode 100644 index 000000000..62d764ef8 --- /dev/null +++ b/internal/services/gateway/action/commitstatusdelivery.go @@ -0,0 +1,84 @@ +// Copyright 2023 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "context" + + "github.com/sorintlab/errors" + + "agola.io/agola/internal/util" + "agola.io/agola/services/notification/client" + nstypes "agola.io/agola/services/notification/types" +) + +type GetProjectCommitStatusDeliveriesRequest struct { + ProjectRef string + DeliveryStatusFilter []string + + Cursor string + + Limit int + SortDirection SortDirection +} + +type GetProjectCommitStatusDeliveriesResponse struct { + CommitStatusDeliveries []*nstypes.CommitStatusDelivery + Cursor string +} + +func (h *ActionHandler) GetProjectCommitStatusDeliveries(ctx context.Context, req *GetProjectCommitStatusDeliveriesRequest) (*GetProjectCommitStatusDeliveriesResponse, error) { + project, _, err := h.configstoreClient.GetProject(ctx, req.ProjectRef) + if err != nil { + return nil, util.NewAPIError(util.KindFromRemoteError(err), err) + } + isUserOwner, err := h.IsAuthUserProjectOwner(ctx, project.OwnerType, project.OwnerID) + if err != nil { + return nil, errors.Wrapf(err, "failed to determine permissions") + } + if !isUserOwner { + return nil, util.NewAPIError(util.ErrForbidden, errors.Errorf("user not authorized")) + } + + inCursor := &StartSequenceCursor{} + sortDirection := req.SortDirection + if req.Cursor != "" { + if err := UnmarshalCursor(req.Cursor, inCursor); err != nil { + return nil, errors.WithStack(err) + } + sortDirection = inCursor.SortDirection + } + + commitStatusDeliveries, resp, err := h.notificationClient.GetProjectCommitStatusDeliveries(ctx, project.ID, &client.GetProjectCommitStatusDeliveriesOptions{ListOptions: &client.ListOptions{Limit: req.Limit, SortDirection: nstypes.SortDirection(sortDirection)}, StartSequence: inCursor.StartSequence, DeliveryStatusFilter: req.DeliveryStatusFilter}) + if err != nil { + return nil, util.NewAPIError(util.KindFromRemoteError(err), err) + } + + var outCursor string + if resp.HasMore && len(commitStatusDeliveries) > 0 { + lastCommitStatusDeliverySequence := commitStatusDeliveries[len(commitStatusDeliveries)-1].Sequence + outCursor, err = MarshalCursor(&StartSequenceCursor{StartSequence: lastCommitStatusDeliverySequence}) + if err != nil { + return nil, errors.WithStack(err) + } + } + + res := &GetProjectCommitStatusDeliveriesResponse{ + CommitStatusDeliveries: commitStatusDeliveries, + Cursor: outCursor, + } + + return res, nil +} diff --git a/internal/services/gateway/api/commitstatusdelivery.go b/internal/services/gateway/api/commitstatusdelivery.go new file mode 100644 index 000000000..df8f20e0c --- /dev/null +++ b/internal/services/gateway/api/commitstatusdelivery.go @@ -0,0 +1,96 @@ +// Copyright 2023 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/rs/zerolog" + "github.com/sorintlab/errors" + + "agola.io/agola/internal/services/gateway/action" + util "agola.io/agola/internal/util" + gwapitypes "agola.io/agola/services/gateway/api/types" + nstypes "agola.io/agola/services/notification/types" +) + +func createCommitStatusDeliveryResponse(r *nstypes.CommitStatusDelivery) *gwapitypes.CommitStatusDeliveryResponse { + commitStatusDelivery := &gwapitypes.CommitStatusDeliveryResponse{ + ID: r.ID, + Sequence: r.Sequence, + DeliveryStatus: gwapitypes.DeliveryStatus(r.DeliveryStatus), + DeliveredAt: r.DeliveredAt, + } + return commitStatusDelivery +} + +type ProjectCommitStatusDeliveries struct { + log zerolog.Logger + ah *action.ActionHandler +} + +func NewProjectCommitStatusDeliveriesHandler(log zerolog.Logger, ah *action.ActionHandler) *ProjectCommitStatusDeliveries { + return &ProjectCommitStatusDeliveries{log: log, ah: ah} +} + +func (h *ProjectCommitStatusDeliveries) ServeHTTP(w http.ResponseWriter, r *http.Request) { + res, err := h.do(w, r) + if util.HTTPError(w, err) { + h.log.Err(err).Send() + return + } + + if err := util.HTTPResponse(w, http.StatusOK, res); err != nil { + h.log.Err(err).Send() + } +} + +func (h *ProjectCommitStatusDeliveries) do(w http.ResponseWriter, r *http.Request) ([]*gwapitypes.CommitStatusDeliveryResponse, error) { + ctx := r.Context() + query := r.URL.Query() + + vars := mux.Vars(r) + projectRef := vars["projectref"] + + deliveryStatusFilter := query["deliverystatus"] + + ropts, err := parseRequestOptions(r) + if err != nil { + return nil, errors.WithStack(err) + } + + areq := &action.GetProjectCommitStatusDeliveriesRequest{ + ProjectRef: projectRef, + + DeliveryStatusFilter: deliveryStatusFilter, + Cursor: ropts.Cursor, + Limit: ropts.Limit, + SortDirection: action.SortDirection(ropts.SortDirection), + } + ares, err := h.ah.GetProjectCommitStatusDeliveries(ctx, areq) + if err != nil { + return nil, errors.WithStack(err) + } + + commitStatusDeliveries := make([]*gwapitypes.CommitStatusDeliveryResponse, len(ares.CommitStatusDeliveries)) + for i, r := range ares.CommitStatusDeliveries { + commitStatusDeliveries[i] = createCommitStatusDeliveryResponse(r) + } + + addCursorHeader(w, ares.Cursor) + + return commitStatusDeliveries, nil +} diff --git a/internal/services/gateway/gateway.go b/internal/services/gateway/gateway.go index 2c63f6798..95ed2ded8 100644 --- a/internal/services/gateway/gateway.go +++ b/internal/services/gateway/gateway.go @@ -201,6 +201,7 @@ func (g *Gateway) Run(ctx context.Context) error { refreshRemoteRepositoryInfoHandler := api.NewRefreshRemoteRepositoryInfoHandler(g.log, g.ah) projectRunWebhookDeliveriesHandler := api.NewProjectRunWebhookDeliveriesHandler(g.log, g.ah) projectRunWebhookRedeliveryHandler := api.NewProjectRunWebhookRedeliveryHandler(g.log, g.ah) + projectCommitStatusDeliveriesHandler := api.NewProjectCommitStatusDeliveriesHandler(g.log, g.ah) secretHandler := api.NewSecretHandler(g.log, g.ah) createSecretHandler := api.NewCreateSecretHandler(g.log, g.ah) @@ -322,6 +323,7 @@ func (g *Gateway) Run(ctx context.Context) error { apirouter.Handle("/projects/{projectref}/refreshremoterepo", authForcedHandler(refreshRemoteRepositoryInfoHandler)).Methods("POST") apirouter.Handle("/projects/{projectref}/runwebhookdeliveries", authForcedHandler(projectRunWebhookDeliveriesHandler)).Methods("GET") apirouter.Handle("/projects/{projectref}/runwebhookdeliveries/{runwebhookdeliveryid}/redelivery", authForcedHandler(projectRunWebhookRedeliveryHandler)).Methods("PUT") + apirouter.Handle("/projects/{projectref}/commitstatusdeliveries", authForcedHandler(projectCommitStatusDeliveriesHandler)).Methods("GET") apirouter.Handle("/projectgroups/{projectgroupref}/secrets", authForcedHandler(secretHandler)).Methods("GET") apirouter.Handle("/projects/{projectref}/secrets", authForcedHandler(secretHandler)).Methods("GET") diff --git a/services/gateway/api/types/commitstatusdelivery.go b/services/gateway/api/types/commitstatusdelivery.go new file mode 100644 index 000000000..2855cf0b9 --- /dev/null +++ b/services/gateway/api/types/commitstatusdelivery.go @@ -0,0 +1,24 @@ +// Copyright 2023 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import "time" + +type CommitStatusDeliveryResponse struct { + ID string `json:"id"` + Sequence uint64 `json:"sequence"` + DeliveryStatus DeliveryStatus `json:"delivery_status"` + DeliveredAt *time.Time `json:"delivered_at"` +} diff --git a/services/gateway/client/client.go b/services/gateway/client/client.go index 2da3f1ca4..898cba15a 100644 --- a/services/gateway/client/client.go +++ b/services/gateway/client/client.go @@ -828,3 +828,16 @@ func (c *Client) GetProjectRunWebhookDeliveries(ctx context.Context, projectRef func (c *Client) ProjectRunWebhookRedelivery(ctx context.Context, projectRef string, runWebhookDeliveryID string) (*Response, error) { return c.getResponse(ctx, "PUT", fmt.Sprintf("/projects/%s/runwebhookdeliveries/%s/redelivery", projectRef, runWebhookDeliveryID), nil, jsonContent, nil) } + +func (c *Client) GetProjectCommitStatusDeliveries(ctx context.Context, projectRef string, deliveryStatusFilter []string, opts *ListOptions) ([]*gwapitypes.CommitStatusDeliveryResponse, *Response, error) { + q := url.Values{} + opts.Add(q) + + for _, deliveryStatus := range deliveryStatusFilter { + q.Add("deliverystatus", deliveryStatus) + } + + commitStatusDeliveries := []*gwapitypes.CommitStatusDeliveryResponse{} + resp, err := c.getParsedResponse(ctx, "GET", fmt.Sprintf("/projects/%s/commitstatusdeliveries", url.PathEscape(projectRef)), q, common.JSONContent, nil, &commitStatusDeliveries) + return commitStatusDeliveries, resp, errors.WithStack(err) +} diff --git a/services/notification/client/client.go b/services/notification/client/client.go index 448add0ce..07aa649a5 100644 --- a/services/notification/client/client.go +++ b/services/notification/client/client.go @@ -144,3 +144,31 @@ func (c *Client) RunWebhookRedelivery(ctx context.Context, projectID, runWebhook resp, err := c.GetResponse(ctx, "PUT", fmt.Sprintf("/projects/%s/runwebhookdeliveries/%s/redelivery", projectID, runWebhookDeliveryID), nil, -1, common.JSONContent, nil) return resp, errors.WithStack(err) } + +type GetProjectCommitStatusDeliveriesOptions struct { + *ListOptions + + StartSequence uint64 + DeliveryStatusFilter []string +} + +func (o *GetProjectCommitStatusDeliveriesOptions) Add(q url.Values) { + o.ListOptions.Add(q) + + if o.StartSequence > 0 { + q.Add("startsequence", strconv.FormatUint(o.StartSequence, 10)) + } +} + +func (c *Client) GetProjectCommitStatusDeliveries(ctx context.Context, projectID string, opts *GetProjectCommitStatusDeliveriesOptions) ([]*types.CommitStatusDelivery, *Response, error) { + q := url.Values{} + opts.Add(q) + + for _, deliveryStatus := range opts.DeliveryStatusFilter { + q.Add("deliverystatus", deliveryStatus) + } + + commitStatusDeliveries := []*types.CommitStatusDelivery{} + resp, err := c.GetParsedResponse(ctx, "GET", fmt.Sprintf("/projects/%s/commitstatusdeliveries", projectID), q, common.JSONContent, nil, &commitStatusDeliveries) + return commitStatusDeliveries, resp, errors.WithStack(err) +} diff --git a/tests/setup_test.go b/tests/setup_test.go index 4ba64be64..5b5b4269c 100644 --- a/tests/setup_test.go +++ b/tests/setup_test.go @@ -7215,3 +7215,239 @@ func TestProjectRunWebhookRedelivery(t *testing.T) { } }) } + +func TestGetProjectCommitStatusDeliveries(t *testing.T) { + t.Parallel() + + config := ` + { + runs: [ + { + name: 'run01', + tasks: [ + { + name: 'task01', + runtime: { + containers: [ + { + image: 'alpine/git', + }, + ], + }, + steps: [ + { type: 'run', command: 'env' }, + ], + }, + ], + }, + ], + } + ` + + dir := t.TempDir() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sc := setup(ctx, t, dir, withGitea(true)) + defer sc.stop() + + giteaToken, tokenUser01 := createLinkedAccount(ctx, t, sc.gitea, sc.config) + gwUser01Client := gwclient.NewClient(sc.config.Gateway.APIExposedURL, tokenUser01) + gwUserAdminClient := gwclient.NewClient(sc.config.Gateway.APIExposedURL, sc.config.Gateway.AdminToken) + + _, _, err := gwUserAdminClient.CreateUser(ctx, &gwapitypes.CreateUserRequest{UserName: agolaUser02}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + user02Token, _, err := gwUserAdminClient.CreateUserToken(ctx, agolaUser02, &gwapitypes.CreateUserTokenRequest{TokenName: "token01"}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + gwUser02Client := gwclient.NewClient(sc.config.Gateway.APIExposedURL, user02Token.Token) + + giteaAPIURL := fmt.Sprintf("http://%s:%s", sc.gitea.HTTPListenAddress, sc.gitea.HTTPPort) + + giteaClient, err := gitea.NewClient(giteaAPIURL, gitea.SetToken(giteaToken)) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + giteaRepo, project := createProject(ctx, t, giteaClient, gwUser01Client, withVisibility(gwapitypes.VisibilityPrivate)) + + push(t, config, giteaRepo.CloneURL, giteaToken, "commit", false) + + runNumbers := 5 + + for i := 0; i < runNumbers-1; i++ { + if _, err = gwUser01Client.ProjectCreateRun(ctx, project.ID, &gwapitypes.ProjectCreateRunRequest{Branch: "master"}); err != nil { + t.Fatalf("unexpected err: %v", err) + } + } + + _ = testutil.Wait(150*time.Second, func() (bool, error) { + runs, _, err := gwUser01Client.GetProjectRuns(ctx, project.ID, nil, nil, 0, 0, true) + if err != nil { + return false, nil + } + + if len(runs) != runNumbers { + return false, nil + } + for i := 0; i < runNumbers; i++ { + if runs[i].Phase != rstypes.RunPhaseFinished { + return false, nil + } + } + + return true, nil + }) + + runs, _, err := gwUser01Client.GetProjectRuns(ctx, project.ID, nil, nil, 0, 0, true) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if len(runs) != runNumbers { + t.Fatalf("expected %d run got: %d", runNumbers, len(runs)) + } + for i := 0; i < runNumbers; i++ { + if runs[i].Phase != rstypes.RunPhaseFinished { + t.Fatalf("expected run phase %q, got %q", rstypes.RunPhaseFinished, runs[i].Phase) + } + if runs[i].Result != rstypes.RunResultSuccess { + t.Fatalf("expected run result %q, got %q", rstypes.RunResultSuccess, runs[i].Result) + } + } + + _ = testutil.Wait(60*time.Second, func() (bool, error) { + commitStatusDeliveries, _, err := gwUser01Client.GetProjectCommitStatusDeliveries(ctx, project.ID, nil, &gwclient.ListOptions{Limit: 0, SortDirection: gwapitypes.SortDirectionAsc}) + if err != nil { + return false, nil + } + + if len(commitStatusDeliveries) != 2*runNumbers { + return false, nil + } + for _, r := range commitStatusDeliveries { + if r.DeliveryStatus != gwapitypes.DeliveryStatusDelivered { + return false, nil + } + } + + return true, nil + }) + + t.Run("test get project commit status deliveries", func(t *testing.T) { + commitStatusDeliveries, resp, err := gwUser01Client.GetProjectCommitStatusDeliveries(ctx, project.ID, nil, &gwclient.ListOptions{Limit: 0, SortDirection: gwapitypes.SortDirectionAsc}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if len(commitStatusDeliveries) != 2*runNumbers { + t.Fatalf("expected %d commitStatusDeliveries got: %d", 2*runNumbers, len(commitStatusDeliveries)) + } + if resp.Cursor != "" { + t.Fatalf("expected empty cursor, got %s", resp.Cursor) + } + for _, r := range commitStatusDeliveries { + if r.DeliveryStatus != gwapitypes.DeliveryStatusDelivered { + t.Fatalf("expected DeliveryStatus delivered, got %s", r.DeliveryStatus) + } + } + }) + + t.Run("test get project commit status deliveries with limit less than project commit status deliveries continuation", func(t *testing.T) { + commitStatusDeliveries, _, err := gwUser01Client.GetProjectCommitStatusDeliveries(ctx, project.ID, nil, &gwclient.ListOptions{SortDirection: gwapitypes.SortDirectionAsc}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + respAllCommitStatusDeliveries := []*gwapitypes.CommitStatusDeliveryResponse{} + + respCommitStatusDeliveries, resp, err := gwUser01Client.GetProjectCommitStatusDeliveries(ctx, project.ID, nil, &gwclient.ListOptions{Limit: 1, SortDirection: gwapitypes.SortDirectionAsc}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + + expectedCommitStatusDeliveries := 1 + if len(respCommitStatusDeliveries) != expectedCommitStatusDeliveries { + t.Fatalf("expected %d project commit status deliveries, got %d project commit status deliveries", expectedCommitStatusDeliveries, len(respCommitStatusDeliveries)) + } + if resp.Cursor == "" { + t.Fatalf("expected cursor, got no cursor") + } + + respAllCommitStatusDeliveries = append(respAllCommitStatusDeliveries, respCommitStatusDeliveries...) + + // fetch next results + for { + respCommitStatusDeliveries, resp, err = gwUser01Client.GetProjectCommitStatusDeliveries(ctx, project.ID, nil, &gwclient.ListOptions{Cursor: resp.Cursor, Limit: 1}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if resp.Cursor != "" && len(respCommitStatusDeliveries) != expectedCommitStatusDeliveries { + t.Fatalf("expected %d project commit status deliveries, got %d project commit status deliveries", expectedCommitStatusDeliveries, len(respCommitStatusDeliveries)) + } + + respAllCommitStatusDeliveries = append(respAllCommitStatusDeliveries, respCommitStatusDeliveries...) + + if resp.Cursor == "" { + break + } + } + + expectedCommitStatusDeliveries = 2 * runNumbers + if len(respAllCommitStatusDeliveries) != expectedCommitStatusDeliveries { + t.Fatalf("expected %d project commit status deliveries, got %d project commit status deliveries", expectedCommitStatusDeliveries, len(respAllCommitStatusDeliveries)) + } + + if diff := cmp.Diff(commitStatusDeliveries, respAllCommitStatusDeliveries); diff != "" { + t.Fatalf("mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("test get project commit status deliveries with user unauthorized", func(t *testing.T) { + _, _, err = gwUser02Client.GetProjectCommitStatusDeliveries(ctx, project.ID, nil, &gwclient.ListOptions{Limit: 0, SortDirection: gwapitypes.SortDirectionAsc}) + if err == nil { + t.Fatalf("expected error %v, got nil err", remoteErrorForbidden) + } + if err.Error() != remoteErrorForbidden { + t.Fatalf("expected err %v, got err: %v", remoteErrorForbidden, err) + } + }) + + t.Run("test get project commit status deliveries with not existing project", func(t *testing.T) { + _, _, err = gwUser01Client.GetProjectCommitStatusDeliveries(ctx, "project02", nil, &gwclient.ListOptions{Limit: 0, SortDirection: gwapitypes.SortDirectionAsc}) + if err == nil { + t.Fatalf("expected error %v, got nil err", remoteErrorNotExist) + } + if err.Error() != remoteErrorNotExist { + t.Fatalf("expected err %v, got err: %v", remoteErrorNotExist, err) + } + }) + + t.Run("test get project commit status deliveries with deliverystatus = delivered", func(t *testing.T) { + commitStatusDeliveries, resp, err := gwUser01Client.GetProjectCommitStatusDeliveries(ctx, project.ID, []string{string(nstypes.DeliveryStatusDelivered)}, &gwclient.ListOptions{Limit: 0, SortDirection: gwapitypes.SortDirectionAsc}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if len(commitStatusDeliveries) != 2*runNumbers { + t.Fatalf("expected %d commitStatusDeliveries got: %d", 2*runNumbers, len(commitStatusDeliveries)) + } + if resp.Cursor != "" { + t.Fatalf("expected empty cursor, got %s", resp.Cursor) + } + }) + + t.Run("test get project commit status deliveries with deliverystatus = deliveryError", func(t *testing.T) { + commitStatusDeliveries, resp, err := gwUser01Client.GetProjectCommitStatusDeliveries(ctx, project.ID, []string{string(nstypes.DeliveryStatusDeliveryError)}, &gwclient.ListOptions{Limit: 0, SortDirection: gwapitypes.SortDirectionAsc}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if len(commitStatusDeliveries) != 0 { + t.Fatalf("expected 0 commitStatusDeliveries got: %d", len(commitStatusDeliveries)) + } + if resp.Cursor != "" { + t.Fatalf("expected empty cursor, got %s", resp.Cursor) + } + }) +}