Thomas Anderson 2024-04-15 02:50:50 +03:00 committed by GitHub
parent b4cd1da29c
commit b8763a8f34
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 144 additions and 1 deletions

View file

@ -2012,6 +2012,53 @@ const docTemplate = `{
}
}
},
"/repos/{repo_id}/logs/{number}/{stepId}": {
"delete": {
"produces": [
"text/plain"
],
"tags": [
"Pipeline logs"
],
"summary": "Deletes step log",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "integer",
"description": "the repository id",
"name": "repo_id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "the number of the pipeline",
"name": "number",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "the step id",
"name": "stepId",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/repos/{repo_id}/move": {
"post": {
"produces": [

View file

@ -224,6 +224,66 @@ func GetStepLogs(c *gin.Context) {
c.JSON(http.StatusOK, logs)
}
// DeleteStepLogs
//
// @Summary Deletes step log
// @Router /repos/{repo_id}/logs/{number}/{stepId} [delete]
// @Produce plain
// @Success 204
// @Tags Pipeline logs
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param repo_id path int true "the repository id"
// @Param number path int true "the number of the pipeline"
// @Param stepId path int true "the step id"
func DeleteStepLogs(c *gin.Context) {
_store := store.FromContext(c)
repo := session.Repo(c)
pipelineNumber, err := strconv.ParseInt(c.Params.ByName("number"), 10, 64)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
_pipeline, err := _store.GetPipelineNumber(repo, pipelineNumber)
if err != nil {
handleDBError(c, err)
return
}
stepID, err := strconv.ParseInt(c.Params.ByName("stepId"), 10, 64)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
_step, err := _store.StepLoad(stepID)
if err != nil {
handleDBError(c, err)
return
}
if _step.PipelineID != _pipeline.ID {
// make sure we cannot read arbitrary logs by id
_ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("step with id %d is not part of repo %s", stepID, repo.FullName))
return
}
switch _step.State {
case model.StatusRunning, model.StatusPending:
c.String(http.StatusUnprocessableEntity, "Cannot delete logs for a pending or running step")
return
}
err = _store.LogDelete(_step)
if err != nil {
handleDBError(c, err)
return
}
c.Status(http.StatusNoContent)
}
// GetPipelineConfig
//
// @Summary Pipeline configuration

View file

@ -104,6 +104,7 @@ func apiRoutes(e *gin.RouterGroup) {
repo.POST("/pipelines/:number/decline", session.MustPush, api.PostDecline)
repo.GET("/logs/:number/:stepId", api.GetStepLogs)
repo.DELETE("/logs/:number/:stepId", session.MustPush, api.DeleteStepLogs)
// requires push permissions
repo.DELETE("/logs/:number", session.MustPush, api.DeletePipelineLogs)

View file

@ -245,6 +245,8 @@
"pipeline": "Pipeline #{pipelineId}",
"log_title": "Step Logs",
"log_download_error": "There was an error while downloading the log file",
"log_delete_confirm": "Do you really want to delete logs of this step?",
"log_delete_error": "There was an error while deleting the step logs",
"actions": {
"cancel": "Cancel",
"restart": "Restart",
@ -253,6 +255,7 @@
"deploy": "Deploy",
"restart_success": "Pipeline restarted",
"log_download": "Download",
"log_delete": "Delete",
"log_auto_scroll": "Automatically scroll down",
"log_auto_scroll_off": "Turn off automatic scrolling"
},

View file

@ -20,6 +20,13 @@
icon="download"
@click="download"
/>
<IconButton
v-if="step?.end_time !== undefined && hasLogs && hasPushPermission"
:title="$t('repo.pipeline.actions.log_delete')"
class="!hover:bg-white !hover:bg-opacity-10"
icon="trash"
@click="deleteLogs"
/>
<IconButton
v-if="step?.end_time === undefined"
:title="
@ -111,7 +118,7 @@ import IconButton from '~/components/atomic/IconButton.vue';
import PipelineStatusIcon from '~/components/repo/pipeline/PipelineStatusIcon.vue';
import useApiClient from '~/compositions/useApiClient';
import useNotifications from '~/compositions/useNotifications';
import { Pipeline, Repo } from '~/lib/api/types';
import { Pipeline, Repo, RepoPermissions } from '~/lib/api/types';
import { findStep, isStepFinished, isStepRunning } from '~/utils/helpers';
type LogLine = {
@ -136,6 +143,7 @@ const i18n = useI18n();
const pipeline = toRef(props, 'pipeline');
const stepId = toRef(props, 'stepId');
const repo = inject<Ref<Repo>>('repo');
const repoPermissions = inject<Ref<RepoPermissions>>('repo-permissions');
const apiClient = useApiClient();
const route = useRoute();
@ -160,6 +168,7 @@ ansiUp.value.use_classes = true;
const logBuffer = ref<LogLine[]>([]);
const maxLineCount = 5000; // TODO(2653): set back to 500 and implement lazy-loading support
const hasPushPermission = computed(() => repoPermissions?.value?.push);
function isSelected(line: LogLine): boolean {
return route.hash === `#L${line.number}`;
@ -297,6 +306,25 @@ async function loadLogs() {
}
}
async function deleteLogs() {
if (!repo?.value || !pipeline.value || !step.value) {
throw new Error('The repository, pipeline or step was undefined');
}
// TODO use proper dialog (copy-pasted from web/src/components/secrets/SecretList.vue:deleteSecret)
// eslint-disable-next-line no-alert, no-restricted-globals
if (!confirm(i18n.t('repo.pipeline.log_delete_confirm'))) {
return;
}
try {
await apiClient.deleteLogs(repo.value.id, pipeline.value.number, step.value.id);
log.value = [];
} catch (e) {
notifications.notifyError(e, i18n.t('repo.pipeline.log_delete_error'));
}
}
onMounted(async () => {
await loadLogs();
});

View file

@ -136,6 +136,10 @@ export default class WoodpeckerClient extends ApiClient {
return this._get(`/api/repos/${repoId}/logs/${pipeline}/${step}`) as Promise<PipelineLog[]>;
}
deleteLogs(repoId: number, pipeline: number, step: number): Promise<unknown> {
return this._delete(`/api/repos/${repoId}/logs/${pipeline}/${step}`);
}
getSecretList(repoId: number, page: number): Promise<Secret[] | null> {
return this._get(`/api/repos/${repoId}/secrets?page=${page}`) as Promise<Secret[] | null>;
}