Help us learn about your current experience with the documentation. Take the survey.

自动化存储管理

  • 层级:免费版、高级版、旗舰版
  • 提供方式:GitLab.com、GitLab 自托管、GitLab 专用实例

本页介绍如何通过 GitLab REST API 自动化存储分析和清理,以管理您的存储使用情况。

您还可以通过提升管道效率来管理存储使用情况。

如需更多关于 API 自动化的帮助,您也可以使用 GitLab 社区论坛和 Discord。

本页中的脚本示例仅用于演示目的,不应在生产环境中使用。您可以使用这些示例设计和测试自己的存储自动化脚本。

API 要求

要自动化存储管理,您的 GitLab.com SaaS 或 GitLab 自托管实例必须能够访问 GitLab REST API

API 鉴权范围

使用以下作用域对 API 进行鉴权:

  • 存储分析:
    • 使用 read_api 作用域获取读取 API 访问权限。
    • 所有项目至少具备开发者角色。
  • 存储清理:
    • 使用 api 作用域获取完全 API 访问权限。
    • 所有项目至少具备维护者角色。

您可以使用命令行工具或编程语言与 REST API 交互。

命令行工具

要发送 API 请求,请安装以下任一工具:

  • 通过您偏好的包管理器安装 curl。
  • GitLab CLI 并使用 glab api 子命令。

要格式化 JSON 响应,请安装 jq。更多信息请参见《高效 DevOps 工作流技巧:使用 jq 格式化 JSON 及 CI/CD 代码检查自动化》。

使用这些工具与 REST API 交互:

export GITLAB_TOKEN=xxx

curl --silent --header "Authorization: Bearer $GITLAB_TOKEN" "https://gitlab.com/api/v4/user" | jq
glab auth login

glab api groups/YOURGROUPNAME/projects

使用 GitLab CLI

某些 API 端点需要分页及后续页面抓取才能获取所有结果。GitLab CLI 提供了 --paginate 标志。

需要以 JSON 数据格式作为 POST 主体内容的请求,可以写成传递给 --raw-field 参数的 key=value 对。

更多信息请参阅 GitLab CLI 端点文档。

API 客户端库

本页所述的存储管理和清理自动化方法使用:

  • python-gitlab 库,它提供了一个功能丰富的编程接口。
  • GitLab API with Python 项目中的 get_all_projects_top_level_namespace_storage_analysis_cleanup_example.py 脚本。

关于 python-gitlab 库用例的更多信息,请参见《高效 DevSecOps 工作流:实践 python-gitlab API 自动化》。

其他 API 客户端库的信息,请参见 第三方客户端

使用 GitLab Duo 代码建议 更高效地编写代码。

存储分析

识别存储类型

项目API端点 为您GitLab实例中的项目提供统计信息。若要使用项目API端点,需将statistics键设置为布尔值true。此数据通过以下存储类型为您提供关于项目存储消耗的洞察:

  • storage_size: 整体存储大小
  • lfs_objects_size: LFS对象存储大小
  • job_artifacts_size: 作业制品存储大小
  • packages_size: 包存储大小
  • repository_size: Git仓库存储大小
  • snippets_size: 片段存储大小
  • uploads_size: 上传文件存储大小
  • wiki_size: Wiki存储大小

若要识别存储类型:

curl --silent --header "Authorization: Bearer $GITLAB_TOKEN" "https://gitlab.com/api/v4/projects/$GL_PROJECT_ID?statistics=true" | jq --compact-output '.id,.statistics' | jq
48349590
{
  "commit_count": 2,
  "storage_size": 90241770,
  "repository_size": 3521,
  "wiki_size": 0,
  "lfs_objects_size": 0,
  "job_artifacts_size": 90238249,
  "pipeline_artifacts_size": 0,
  "packages_size": 0,
  "snippets_size": 0,
  "uploads_size": 0
}
export GL_PROJECT_ID=48349590
glab api --method GET projects/$GL_PROJECT_ID --field 'statistics=true' | jq --compact-output '.id,.statistics' | jq
48349590
{
  "commit_count": 2,
  "storage_size": 90241770,
  "repository_size": 3521,
  "wiki_size": 0,
  "lfs_objects_size": 0,
  "job_artifacts_size": 90238249,
  "pipeline_artifacts_size": 0,
  "packages_size": 0,
  "snippets_size": 0,
  "uploads_size": 0
}
project_obj = gl.projects.get(project.id, statistics=True)

print("Project {n} statistics: {s}".format(n=project_obj.name_with_namespace, s=json.dump(project_obj.statistics, indent=4)))

要将项目的统计信息打印到终端,导出GL_GROUP_ID环境变量并运行脚本:

export GL_TOKEN=xxx
export GL_GROUP_ID=56595735

pip3 install python-gitlab
python3 get_all_projects_top_level_namespace_storage_analysis_cleanup_example.py

Project Developer Evangelism and Technical Marketing at GitLab  / playground / Artifact generator group / Gen Job Artifacts 4 statistics: {
    "commit_count": 2,
    "storage_size": 90241770,
    "repository_size": 3521,
    "wiki_size": 0,
    "lfs_objects_size": 0,
    "job_artifacts_size": 90238249,
    "pipeline_artifacts_size": 0,
    "packages_size": 0,
    "snippets_size": 0,
    "uploads_size": 0
}

分析项目和组中的存储

您可以自动化分析多个项目和组。例如,您可以从顶级命名空间级别开始,递归分析所有子组和项目。您也可以分析不同的存储类型。

以下是分析多个子组和项目的算法示例:

  1. 获取顶级命名空间ID。您可以从命名空间/组概览中复制ID值。
  2. 从顶级组中获取所有子组,并将ID保存到列表中。
  3. 遍历所有组,获取每个组的所有项目,并将ID保存到列表中。
  4. 确定要分析的存储类型,并从项目属性(如项目统计信息和作业制品)中收集信息。
  5. 打印所有项目的概览,按组分组,及其存储信息。

使用glab的Shell方法可能更适合较小的分析。对于较大的分析,您应该使用利用API客户端库的脚本。这类脚本可提升可读性、数据存储、流程控制、测试及复用性。

为确保脚本不会触发API速率限制,以下示例代码未针对并行API请求做优化。

若要实现该算法:

export GROUP_NAME="gitlab-da"

# 返回子组ID
glab api groups/$GROUP_NAME/subgroups | jq --compact-output '.[]' | jq --compact-output '.id'
12034712
67218622
67162711
67640130
16058698
12034604

# 遍历所有子组以获取子组,直至结果集为空。示例组:12034712
glab api groups/12034712/subgroups | jq --compact-output '.[]' | jq --compact-output '.id'
56595735
70677315
67218606
70812167

# 最低组层级
glab api groups/56595735/subgroups | jq --compact-output '.[]' | jq --compact-output '.id'

# 结果为空,返回并继续分析

# 从所有收集的组中获取项目。示例组:56595735
glab api groups/56595735/projects | jq --compact-output '.[]' | jq --compact-output '.id'
48349590
48349263
38520467
38520405

# 从项目(ID 48349590)获取存储类型:作业工件位于 `artifacts` 键中  
glab api projects/48349590/jobs | jq --compact-output '.[]' | jq --compact-output '.id, .artifacts'  
4828297946  
[{"file_type":"archive","size":52444993,"filename":"artifacts.zip","file_format":"zip"},{"file_type":"metadata","size":156,"filename":"metadata.gz","file_format":"gzip"},{"file_type":"trace","size":3140,"filename":"job.log","file_format":null}]  
4828297945  
[{"file_type":"archive","size":20978113,"filename":"artifacts.zip","file_format":"zip"},{"file_type":"metadata","size":157,"filename":"metadata.gz","file_format":"gzip"},{"file_type":"trace","size":3147,"filename":"job.log","file_format":null}]  
4828297944  
[{"file_type":"archive","size":10489153,"filename":"artifacts.zip","file_format":"zip"},{"file_type":"metadata","size":158,"filename":"metadata.gz","file_format":"gzip"},{"file_type":"trace","size":3146,"filename":"job.log","file_format":null}]  
4828297943  
[{"file_type":"archive","size":5244673,"filename":"artifacts.zip","file_format":"zip"},{"file_type":"metadata","size":157,"filename":"metadata.gz","file_format":"gzip"},{"file_type":"trace","size":3145,"filename":"job.log","file_format":null}]  
4828297940  
[{"file_type":"archive","size":1049089,"filename":"artifacts.zip","file_format":"zip"},{"file_type":"metadata","size":157,"filename":"metadata.gz","file_format":"gzip"},{"file_type":"trace","size":3140,"filename":"job.log","file_format":null}]  
#!/usr/bin/env python

import datetime
import gitlab
import os
import sys

GITLAB_SERVER = os.environ.get('GL_SERVER', 'https://gitlab.com')
GITLAB_TOKEN = os.environ.get('GL_TOKEN') # token requires developer permissions
PROJECT_ID = os.environ.get('GL_PROJECT_ID') #optional
GROUP_ID = os.environ.get('GL_GROUP_ID') #optional

if __name__ == "__main__":
    if not GITLAB_TOKEN:
        print("🤔 Please set the GL_TOKEN env variable.")
        sys.exit(1)

    gl = gitlab.Gitlab(GITLAB_SERVER, private_token=GITLAB_TOKEN, pagination="keyset", order_by="id", per_page=100)

    # Collect all projects, or prefer projects from a group id, or a project id
    projects = []

    # Direct project ID
    if PROJECT_ID:
        projects.append(gl.projects.get(PROJECT_ID))
    # Groups and projects inside
    elif GROUP_ID:
        group = gl.groups.get(GROUP_ID)

        for project in group.projects.list(include_subgroups=True, get_all=True):
            manageable_project = gl.projects.get(project.id , lazy=True)
            projects.append(manageable_project)

    for project in projects:
        jobs = project.jobs.list(pagination="keyset", order_by="id", per_page=100, iterator=True)
        for job in jobs:
            print("DEBUG: ID {i}: {a}".format(i=job.id, a=job.attributes['artifacts'])

该脚本以JSON格式列表输出项目的作业工件:

[
    {
        "file_type": "archive",
        "size": 1049089,
        "filename": "artifacts.zip",
        "file_format": "zip"
    },
    {
        "file_type": "metadata",
        "size": 157,
        "filename": "metadata.gz",
        "file_format": "gzip"
    },
    {
        "file_type": "trace",
        "size": 3146,
        "filename": "job.log",
        "file_format": null
    }
]

管理CI/CD流水线存储

作业工件消耗了大部分流水线存储,而作业日志也可能生成数百千字节的数据。你应该先删除不必要的作业工件,然后在分析后清理作业日志。

删除作业日志和工件是一项破坏性操作,无法撤销。请谨慎使用。删除某些文件(包括报告工件、作业日志和元数据文件)会影响使用这些文件作为数据源的GitLab功能。

列出作业工件

要分析流水线存储,你可以使用作业API端点来获取作业工件列表。该端点在artifacts属性中返回作业工件的file_type键。file_type键表示工件类型:

  • archive用于生成的作业工件作为zip文件。
  • metadata用于Gzip文件中的额外元数据。
  • trace用于原始文件的job.log

作业工件提供了一个可写入磁盘缓存文件的数据结构,你可以用它来测试实现。

基于获取所有项目的示例代码,你可以扩展Python脚本来做更多分析。

以下示例显示了对项目中作业工件查询的响应:

[
    {
        "file_type": "archive",
        "size": 1049089,
        "filename": "artifacts.zip",
        "file_format": "zip"
    },
    {
        "file_type": "metadata",
        "size": 157,
        "filename": "metadata.gz",
        "file_format": "gzip"
    },
    {
        "file_type": "trace",
        "size": 3146,
        "filename": "job.log",
        "file_format": null
    }
]

根据你实现脚本的方式,你可以选择:

  • 收集所有作业工件并在脚本末尾打印摘要表格。
  • 立即打印信息。

在以下示例中,作业工件被收集到ci_job_artifacts列表中。脚本遍历所有项目并获取:

  • 包含所有属性的project_obj对象变量。
  • 来自job对象的artifacts属性。

你可以使用keyset分页来迭代大型流水线和作业列表。

   ci_job_artifacts = []

    for project in projects:
        project_obj = gl.projects.get(project.id)

        jobs = project.jobs.list(pagination="keyset", order_by="id", per_page=100, iterator=True)

        for job in jobs:
            artifacts = job.attributes['artifacts']
            #print("DEBUG: ID {i}: {a}".format(i=job.id, a=json.dumps(artifacts, indent=4)))
            if not artifacts:
                continue

            for a in artifacts:
                data = {
                    "project_id": project_obj.id,
                    "project_web_url": project_obj.name,
                    "project_path_with_namespace": project_obj.path_with_namespace,
                    "job_id": job.id,
                    "artifact_filename": a['filename'],
                    "artifact_file_type": a['file_type'],
                    "artifact_size": a['size']
                }

                ci_job_artifacts.append(data)

    print("\nDone collecting data.")

    if len(ci_job_artifacts) > 0:
        print("| Project | Job | Artifact name | Artifact type | Artifact size |\n|---------|-----|---------------|---------------|---------------|") # Start markdown friendly table
        for artifact in ci_job_artifacts:
            print('| [{project_name}]({project_web_url}) | {job_name} | {artifact_name} | {artifact_type} | {artifact_size} |'.format(project_name=artifact['project_path_with_namespace'], project_web_url=artifact['project_web_url'], job_name=artifact['job_id'], artifact_name=artifact['artifact_filename'], artifact_type=artifact['artifact_file_type'], artifact_size=render_size_mb(artifact['artifact_size'])))
    else:
        print("No artifacts found.")

脚本末尾,作业工件以Markdown格式的表格形式打印出来。你可以将表格内容复制到问题评论或描述中,或在GitLab仓库中填充一个Markdown文件。

$ python3 get_all_projects_top_level_namespace_storage_analysis_cleanup_example.py

| Project | Job | Artifact name | Artifact type | Artifact size |
|---------|-----|---------------|---------------|---------------|
| [gitlab-da/playground/artifact-gen-group/gen-job-artifacts-4](Gen Job Artifacts 4) | 4828297946 | artifacts.zip | archive | 50.0154 |
| [gitlab-da/playground/artifact-gen-group/gen-job-artifacts-4](Gen Job Artifacts 4) | 4828297946 | metadata.gz | metadata | 0.0001 |
| [gitlab-da/playground/artifact-gen-group/gen-job-artifacts-4](Gen Job Artifacts 4) | 4828297946 | job.log | trace | 0.0030 |
| [gitlab-da/playground/artifact-gen-group/gen-job-artifacts-4](Gen Job Artifacts 4) | 4828297945 | artifacts.zip | archive | 20.0063 |
| [gitlab-da/playground/artifact-gen-group/gen-job-artifacts-4](Gen Job Artifacts 4) | 4828297945 | metadata.gz | metadata | 0.0001 |
| [gitlab-da/playground/artifact-gen-group/gen-job-artifacts-4](Gen Job Artifacts 4) | 4828297945 | job.log | trace | 0.0030 |

批量删除作业工件

你可以使用一个 Python 脚本来过滤要批量删除的作业工件类型。

过滤 API 查询结果以进行比较:

  • 使用 created_at 值计算工件的年龄。
  • 使用 size 属性确定工件是否符合大小阈值。

典型请求:

  • 删除早于指定天数的作业工件。
  • 删除超过指定存储量的作业工件。例如,100 MB。

在下面的示例中,脚本遍历作业属性并标记它们以供删除。当集合循环移除对象锁时,脚本会删除标记为删除的作业工件。

   for project in projects:
        project_obj = gl.projects.get(project.id)

        jobs = project.jobs.list(pagination="keyset", order_by="id", per_page=100, iterator=True)

        for job in jobs:
            artifacts = job.attributes['artifacts']
            if not artifacts:
                continue

            # 高级过滤:年龄和大小
            # 示例:90 天,10 MB 阈值(TODO:使其可配置)
            threshold_age = 90 * 24 * 60 * 60
            threshold_size = 10 * 1024 * 1024

            # 作业年龄,需解析 API 格式:2023-08-08T22:41:08.270Z
            created_at = datetime.datetime.strptime(job.created_at, '%Y-%m-%dT%H:%M:%S.%fZ')
            now = datetime.datetime.now()
            age = (now - created_at).total_seconds()
            # 更简短:使用函数
            # age = calculate_age(job.created_at)

            for a in artifacts:
                # 为可读性移除了分析集合代码

                # 高级过滤:将作业工件的年龄和大小与阈值进行比较
                if (float(age) > float(threshold_age)) or (float(a['size']) > float(threshold_size)):
                    # 标记作业以供删除(不能在循环内删除)
                    jobs_marked_delete_artifacts.append(job)

    print("\n数据收集完成。")

    # 高级过滤:删除所有标记为删除的作业工件。
    for job in jobs_marked_delete_artifacts:
        # 删除工件
        print("DEBUG", job)
        job.delete_artifacts()

    # 打印集合摘要(为可读性移除)

删除项目的所有作业工件

如果你不需要项目的作业工件,可以使用以下命令删除所有作业工件。此操作无法撤销。

工件删除可能需要几分钟或几小时,具体取决于要删除的工件数量。后续针对 API 的分析查询可能会返回工件作为误报结果。为了避免结果混淆,请不要立即运行额外的 API 请求。

默认情况下,会保留最近成功作业的工件

要删除项目的所有作业工件:

export GL_PROJECT_ID=48349590

curl --silent --header "Authorization: Bearer $GITLAB_TOKEN" --request DELETE "https://gitlab.com/api/v4/projects/$GL_PROJECT_ID/artifacts"
glab api --method GET projects/$GL_PROJECT_ID/jobs | jq --compact-output '.[]' | jq --compact-output '.id, .artifacts'

glab api --method DELETE projects/$GL_PROJECT_ID/artifacts
        project.artifacts.delete()

删除作业日志

当你删除作业日志时,也会擦除整个作业

使用 GitLab CLI 的示例:

glab api --method GET projects/$GL_PROJECT_ID/jobs | jq --compact-output '.[]' | jq --compact-output '.id'

4836226184
4836226183
4836226181
4836226180

glab api --method POST projects/$GL_PROJECT_ID/jobs/4836226180/erase | jq --compact-output '.name,.status'
"generate-package: [1]"
"success"

python-gitlab API 库中,使用 job.erase() 代替 job.delete_artifacts()。为了避免这个 API 调用被阻止,请将脚本设置为在删除作业工件的调用之间休眠很短的时间:

    for job in jobs_marked_delete_artifacts:
        # 删除工件和作业日志
        print("DEBUG", job)
        #job.delete_artifacts()
        job.erase()
        # 休眠 1 秒
        time.sleep(1)

支持创建作业日志保留策略的建议已在 issue 374717 中提出。

删除旧流水线

流水线不会增加整体存储使用量,但如果需要,你可以自动化删除它们

若要根据特定日期删除流水线,请指定 created_at 键。 你可以用该日期计算当前日期与流水线创建时间的差值。如果年龄超过阈值,则删除该流水线。

created_at 键必须从时间戳转换为 Unix 时间戳(epoch time),例如使用 date -d '2023-08-08T18:59:47.581Z' +%s

GitLab CLI 示例:

export GL_PROJECT_ID=48349590

glab api --method GET projects/$GL_PROJECT_ID/pipelines | jq --compact-output '.[]' | jq --compact-output '.id,.created_at'
960031926
"2023-08-08T22:09:52.745Z"
959884072
"2023-08-08T18:59:47.581Z"

glab api --method DELETE projects/$GL_PROJECT_ID/pipelines/960031926

glab api --method GET projects/$GL_PROJECT_ID/pipelines | jq --compact-output '.[]' | jq --compact-output '.id,.created_at'
959884072
"2023-08-08T18:59:47.581Z"

在下面的 Bash 脚本示例中:

  • 已安装并授权 jq 和 GitLab CLI。
  • 导出的环境变量 GL_PROJECT_ID。默认值为 GitLab 预定义变量 CI_PROJECT_ID
  • 导出的环境变量 CI_SERVER_HOST 指向 GitLab 实例的 URL。

完整脚本 get_cicd_pipelines_compare_age_threshold_example.sh 位于 GitLab API with Linux Shell 项目中。

#!/bin/bash

# 所需程序:

# - GitLab CLI (glab): https://docs.gitlab.com/ee/editor_extensions/gitlab_cli/

# - jq: https://jqlang.github.io/jq/

# 所需变量:

# - PAT: Project Access Token with API scope and Owner role, or Personal Access Token with API scope

# - GL_PROJECT_ID: ID of the project where pipelines must be cleaned

# - AGE_THRESHOLD (optional): Maximum age in days of pipelines to keep (default: 90)

set -euo pipefail

# 常量
DEFAULT_AGE_THRESHOLD=90
SECONDS_PER_DAY=$((24 * 60 * 60))

# 函数
log_info() {
    echo "[信息] $1"
}

log_error() {
    echo "[错误] $1" >&2
}

delete_pipeline() {
    local project_id=$1
    local pipeline_id=$2
    if glab api --method DELETE "projects/$project_id/pipelines/$pipeline_id"; then
        log_info "已删除流水线 ID $pipeline_id"
    else
        log_error "删除流水线 ID $pipeline_id 失败"
    fi
}

# 主脚本
main() {
    # 认证
    if ! glab auth login --hostname "$CI_SERVER_HOST" --token "$PAT"; then
        log_error "认证失败"
        exit 1
    fi

    # 设置变量
    AGE_THRESHOLD=${AGE_THRESHOLD:-$DEFAULT_AGE_THRESHOLD}
    AGE_THRESHOLD_IN_SECONDS=$((AGE_THRESHOLD * SECONDS_PER_DAY))
    GL_PROJECT_ID=${GL_PROJECT_ID:-$CI_PROJECT_ID}

    # 获取流水线
    PIPELINES=$(glab api --method GET "projects/$GL_PROJECT_ID/pipelines")
    if [ -z "$PIPELINES" ]; then
        log_error "获取流水线失败或未找到流水线"
        exit 1
    fi

    # 处理流水线
    echo "$PIPELINES" | jq -r '.[] | [.id, .created_at] | @tsv' | while IFS=$'\t' read -r id created_at; do
        CREATED_AT_TS=$(date -d "$created_at" +%s)
        NOW=$(date +%s)
        AGE=$((NOW - CREATED_AT_TS))

        if [ "$AGE" -gt "$AGE_THRESHOLD_IN_SECONDS" ]; then
            log_info "流水线 ID $id 创建于 $created_at,超过阈值 $AGE_THRESHOLD 天,正在删除..."
            delete_pipeline "$GL_PROJECT_ID" "$id"
        else
            log_info "流水线 ID $id 创建于 $created_at,未超过阈值 $AGE_THRESHOLD 天。忽略。"
        fi
    done
}

main

完整脚本 cleanup-old-pipelines.sh 位于 GitLab API with Linux Shell 项目中。

#!/bin/bash

set -euo pipefail

# 所需环境变量:

# PAT: Project Access Token with API scope and Owner role, or Personal Access Token with API scope.

# 可选环境变量:

# AGE_THRESHOLD: Maximum age (in days) of pipelines to keep. Default: 90 days.

# REPO: Repository to clean up. If not set, the current repository will be used.

# CI_SERVER_HOST: GitLab server hostname.

# 显示错误消息并退出的函数
error_exit() {
    echo "错误: $1" >&2
    exit 1
}

# 验证所需环境变量
[[ -z "${PAT:-}" ]] && error_exit "PAT (项目访问令牌或个人访问令牌) 未设置。"
[[ -z "${CI_SERVER_HOST:-}" ]] && error_exit "CI_SERVER_HOST 未设置。"


# 设置并验证 AGE_THRESHOLD
AGE_THRESHOLD=${AGE_THRESHOLD:-90}
[[ ! "$AGE_THRESHOLD" =~ ^[0-9]+$ ]] && error_exit "AGE_THRESHOLD 必须是一个正整数。"

AGE_THRESHOLD_IN_HOURS=$((AGE_THRESHOLD * 24))

echo "正在删除超过 $AGE_THRESHOLD 天的流水线"

# 使用 GitLab 进行身份验证
glab auth login --hostname "$CI_SERVER_HOST" --token "$PAT" || error_exit "身份验证失败"

# 删除旧的流水线
delete_cmd="glab ci delete --older-than ${AGE_THRESHOLD_IN_HOURS}h"
if [[ -n "${REPO:-}" ]]; then
    delete_cmd+=" --repo $REPO"
fi

$delete_cmd || error_exit "流水线删除失败"

echo "流水线清理完成。"

您也可以使用 python-gitlab API 库created_at 属性来实现一个类似的算法,用于比较作业工件的年龄:

        # ...

        for pipeline in project.pipelines.list(iterator=True):
            pipeline_obj = project.pipelines.get(pipeline.id)
            print("DEBUG: {p}".format(p=json.dumps(pipeline_obj.attributes, indent=4)))

            created_at = datetime.datetime.strptime(pipeline.created_at, '%Y-%m-%dT%H:%M:%S.%fZ')
            now = datetime.datetime.now()
            age = (now - created_at).total_seconds()

            threshold_age = 90 * 24 * 60 * 60

            if (float(age) > float(threshold_age)):
                print("删除流水线", pipeline.id)
                pipeline_obj.delete()

列出作业工件的过期设置

要管理工件存储,您可以更新或配置当工件过期时的时间。

工件的过期设置是在每个作业配置中的 .gitlab-ci.yml 中配置的。

如果有多个项目,且基于CI/CD配置中作业定义的组织方式,可能难以定位过期设置。您可使用脚本来搜索整个CI/CD配置。这包括访问继承值(如 extends!reference)后解析的对象。

该脚本会获取合并后的CI/CD配置文件,并搜索 artifacts 键以:

  • 识别未配置过期设置的作业。
  • 返回已配置工件过期的作业的过期设置。

以下流程说明脚本如何搜索工件过期设置:

  1. 为生成合并的CI/CD配置,脚本遍历所有项目并调用 ci_lint() 方法。
  2. yaml_load 函数将合并配置加载至Python数据结构以供进一步分析。
  3. 包含 script 键的字典会被识别为作业定义,其中可能存在 artifacts 键。
  4. 若存在,脚本会解析子键 expire_in 并存储详情,后续用于打印至Markdown表格摘要。
    ci_job_artifacts_expiry = {}

    # 遍历项目,获取 .gitlab-ci.yml,运行linter以获取完整转换后的配置,并提取 `artifacts:` 设置
    # https://python-gitlab.readthedocs.io/en/stable/gl_objects/ci_lint.html
    for project in projects:
            project_obj = gl.projects.get(project.id)
            project_name = project_obj.name
            project_web_url = project_obj.web_url
            try:
                lint_result = project.ci_lint.get()
                if lint_result.merged_yaml is None:
                    continue

                ci_pipeline = yaml.safe_load(lint_result.merged_yaml)
                #print("Project {p} Config\n{c}\n\n".format(p=project_name, c=json.dumps(ci_pipeline, indent=4)))

                for k in ci_pipeline:
                    v = ci_pipeline[k]
                    # 这是带有 `script` 属性的作业对象
                    if isinstance(v, dict) and 'script' in v:
                        print(".", end="", flush=True) # 输出反馈以表明仍在循环
                        artifacts = v['artifacts'] if 'artifacts' in v else {}

                        print("Project {p} job {j} artifacts {a}".format(p=project_name, j=k, a=json.dumps(artifacts, indent=4)))

                        expire_in = None
                        if 'expire_in' in artifacts:
                            expire_in = artifacts['expire_in']

                        store_key = project_web_url + '_' + k
                        ci_job_artifacts_expiry[store_key] = { 'project_web_url': project_web_url,
                                                        'project_name': project_name,
                                                        'job_name': k,
                                                        'artifacts_expiry': expire_in}

            except Exception as e:
                 print(f"Exception searching artifacts on ci_pipelines: {e}".format(e=e))

    if len(ci_job_artifacts_expiry) > 0:
        print("| 项目 | 作业 | 工件过期时间 |\n|---------|-----|-----------------|") # 开始Markdown兼容表格
        for k, details in ci_job_artifacts_expiry.items():
            if details['job_name'][0] == '.':
                continue # 忽略以 '.' 开头的作业模板
            print(f'| [{ details["project_name"] }]({details["project_web_url"]}) | { details["job_name"] } | { details["artifacts_expiry"] if details["artifacts_expiry"] is not None else "❌ N/A" } |')

该脚本生成的Markdown摘要表格包含:

  • 项目名称与URL。
  • 作业名称。
  • artifacts:expire_in 设置(若未配置则为 N/A)。

该脚本不会打印以下作业模板:

  • . 字符开头的。
  • 未实例化为运行时生成工件的作业对象的。
export GL_GROUP_ID=56595735

安装脚本依赖

python3 -m pip install ‘python-gitlab[yaml]’

python3 get_all_cicd_config_artifacts_expiry.py

项目 任务 工件过期时间
Gen Job Artifacts 4 generator 30 天
Gen Job Artifacts with expiry and included jobs included-job10 10 天
Gen Job Artifacts with expiry and included jobs included-job1 1 天
Gen Job Artifacts with expiry and included jobs included-job30 30 天
Gen Job Artifacts with expiry and included jobs generator 30 天
Gen Job Artifacts 2 generator ❌ N/A
Gen Job Artifacts 1 generator ❌ N/A

get_all_cicd_config_artifacts_expiry.py 脚本位于 GitLab API with Python 项目 中。

或者,你可以使用 高级搜索 结合 API 请求。以下示例使用 scope: blobs 在所有 *.yml 文件中搜索字符串 artifacts

# https://gitlab.com/gitlab-da/playground/artifact-gen-group/gen-job-artifacts-expiry-included-jobs
export GL_PROJECT_ID=48349263

glab api --method GET projects/$GL_PROJECT_ID/search --field "scope=blobs" --field "search=expire_in filename:*.yml"

有关库存方法的更多信息,请参阅 GitLab 如何帮助缓解 Docker Hub 上开源容器镜像的删除

设置作业工件的默认过期时间

要在项目中设置作业工件的默认过期时间,请在 .gitlab-ci.yml 文件中指定 expire_in 值:

default:
    artifacts:
        expire_in: 1 week

管理容器注册表存储

容器注册表可用于 项目群组。你可以分析这两个位置来实施清理策略。

列出容器注册表

要列出项目中的容器注册表:

export GL_PROJECT_ID=48057080

curl --silent --header "Authorization: Bearer $GITLAB_TOKEN" "https://gitlab.com/api/v4/projects/$GL_PROJECT_ID/registry/repositories" | jq --compact-output '.[]' | jq --compact-output '.id,.location' | jq
4435617
"registry.gitlab.com/gitlab-da/playground/container-package-gen-group/docker-alpine-generator"

curl --silent --header "Authorization: Bearer $GITLAB_TOKEN" "https://gitlab.com/api/v4/registry/repositories/4435617?size=true" | jq --compact-output '.id,.location,.size'
4435617
"registry.gitlab.com/gitlab-da/playground/container-package-gen-group/docker-alpine-generator"
3401613
export GL_PROJECT_ID=48057080

glab api --method GET projects/$GL_PROJECT_ID/registry/repositories | jq --compact-output '.[]' | jq --compact-output '.id,.location'
4435617
"registry.gitlab.com/gitlab-da/playground/container-package-gen-group/docker-alpine-generator"

glab api --method GET registry/repositories/4435617 --field='size=true' | jq --compact-output '.id,.location,.size'
4435617
"registry.gitlab.com/gitlab-da/playground/container-package-gen-group/docker-alpine-generator"
3401613

glab api --method GET projects/$GL_PROJECT_ID/registry/repositories/4435617/tags | jq --compact-output '.[]' | jq --compact-output '.name'
"latest"

glab api --method GET projects/$GL_PROJECT_ID/registry/repositories/4435617/tags/latest | jq --compact-output '.name,.created_at,.total_size'
"latest"
"2023-08-07T19:20:20.894+00:00"
3401613

批量删除容器镜像

当你批量删除容器镜像标签时, 你可以配置:

  • 要保留(name_regex_keep)或删除(name_regex_delete)的标签名称和镜像的匹配正则表达式
  • 与标签名称匹配的镜像标签数量(keep_n
  • 镜像标签可被删除的天数前(older_than

在 GitLab.com 上,由于容器注册表的规模,此 API 删除的标签数量有限。
如果你的容器注册表有大量要删除的标签,只会删除其中一部分。你可能需要
多次调用 API。若要安排标签自动删除,请改用清理策略

以下示例使用python-gitlab API 库获取标签列表,并使用筛选参数调用 delete_in_bulk() 方法。

        repositories = project.repositories.list(iterator=True, size=True)
        if len(repositories) > 0:
            repository = repositories.pop()
            tags = repository.tags.list()

            # 清理:只保留最新的标签  
            repository.tags.delete_in_bulk(keep_n=1)
            # 清理:删除所有超过 1 个月的标签  
            repository.tags.delete_in_bulk(older_than="1m")
            # 清理:删除所有匹配正则表达式 `v.*` 的标签,并保留最新的 2 个标签  
            repository.tags.delete_in_bulk(name_regex_delete="v.+", keep_n=2)

为容器创建清理策略

使用项目 REST API 端点来创建清理策略
设置清理策略后,所有符合你指定条件的容器镜像会自动删除。你无需额外的 API 自动化脚本。

若要将属性作为请求体参数发送:

  • 使用 --input - 参数从标准输入读取。
  • 设置 Content-Type 标头。

以下示例使用 GitLab CLI 创建清理策略:

export GL_PROJECT_ID=48057080

echo '{"container_expiration_policy_attributes":{"cadence":"1month","enabled":true,"keep_n":1,"older_than":"14d","name_regex":".*","name_regex_keep":".*-main"}}' | glab api --method PUT --header 'Content-Type: application/json;charset=UTF-8' projects/$GL_PROJECT_ID --input -

...

  "container_expiration_policy": {
    "cadence": "1month",
    "enabled": true,
    "keep_n": 1,
    "older_than": "14d",
    "name_regex": ".*",
    "name_regex_keep": ".*-main",
    "next_run_at": "2023-09-08T21:16:25.354Z"
  },

优化容器镜像

你可以优化容器镜像以减少镜像大小及容器注册表的整体存储消耗。更多信息请参阅流水线效率文档

管理包注册表存储

包注册表可用于项目

列出包和文件

以下示例展示了如何使用 GitLab CLI 从指定项目 ID 获取包。结果集是一个字典项数组,可通过 jq 命令链进行筛选。


# https://gitlab.com/gitlab-da/playground/container-package-gen-group/generic-package-generator
export GL_PROJECT_ID=48377643

glab api --method GET projects/$GL_PROJECT_ID/packages | jq --compact-output '.[]' | jq --compact-output '.id,.name,.package_type'
16669383
"generator"
"generic"
16671352
"generator"
"generic"
16672235
"generator"
"generic"
16672237
"generator"
"generic"

使用包 ID 检查包内的文件及其大小。

glab api --method GET projects/$GL_PROJECT_ID/packages/16669383/package_files | jq --compact-output '.[]' |
 jq --compact-output '.package_id,.file_name,.size'

16669383
"nighly.tar.gz"
10487563

类似的自动化 Shell 脚本在删除旧流水线部分中创建。

以下脚本示例使用 python-gitlab 库循环获取所有包,
并遍历其包文件以打印 file_namesize 属性。

        packages = project.packages.list(order_by="created_at")

        for package in packages:

            package_files = package.package_files.list()
            for package_file in package_files:
                print("Package name: {p} File name: {f} Size {s}".format(
                    p=package.name, f=package_file.file_name, s=render_size_mb(package_file.size)))

删除软件包

在软件包中删除文件可能会损坏该软件包。在进行自动化清理维护时,你应该删除该软件包。

要删除软件包,请使用 GitLab CLI 将 --method 参数更改为 DELETE

glab api --method DELETE projects/$GL_PROJECT_ID/packages/16669383

若要计算软件包大小并与阈值比较,你可以使用 python-gitlab 库扩展 列出软件包及文件 部分描述的代码。

以下代码示例还会计算软件包的年龄,并在条件匹配时删除软件包:

        packages = project.packages.list(order_by="created_at")
        for package in packages:
            package_size = 0.0

            package_files = package.package_files.list()
            for package_file in package_files:
                print("软件包名称: {p} 文件名: {f} 大小 {s}".format(
                    p=package.name, f=package_file.file_name, s=render_size_mb(package_file.size)))

                package_size =+ package_file.size

            print("软件包大小: {s}\n\n".format(s=render_size_mb(package_size)))

            threshold_size = 10 * 1024 * 1024

            if (package_size > float(threshold_size)):
                print("软件包大小 {s} > 阈值 {t}, 正在删除软件包。".format(
                    s=render_size_mb(package_size), t=render_size_mb(threshold_size)))
                package.delete()

            threshold_age = 90 * 24 * 60 * 60
            package_age = created_at = calculate_age(package.created_at)

            if (float(package_age > float(threshold_age))):
                print("软件包年龄 {a} > 阈值 {t}, 正在删除软件包。".format(
                    a=render_age_time(package_age), t=render_age_time(threshold_age)))
                package.delete()

该代码会生成如下输出,你可将其用于进一步分析:

软件包名称: generator 文件名: nighly.tar.gz 大小 10.0017
软件包大小: 10.0017
软件包大小 10.0017 > 阈值 10.0000, 正在删除软件包。

软件包名称: generator 文件名: 1-nightly.tar.gz 大小 1.0004
软件包大小: 1.0004

软件包名称: generator 文件名: 10-nightly.tar.gz 大小 10.0018
软件包名称: generator 文件名: 20-nightly.tar.gz 大小 20.0033
软件包大小: 20.0033
软件包大小 20.0033 > 阈值 10.0000, 正在删除软件包。

依赖代理

查看cleanup policy 以及如何通过API清除缓存

提高输出可读性

你可能需要将时间戳秒数转换为时长格式,或以更具代表性的方式打印原始字节。你可以使用以下辅助函数来转换数值以提高可读性:

# 当前 Unix 时间戳
date +%s

# 将带时区的 `created_at` 日期时间转换为 Unix 时间戳
date -d '2023-08-08T18:59:47.581Z' +%s

使用 python-gitlab API 库的 Python 示例:

def render_size_mb(v):
    return "%.4f" % (v / 1024 / 1024)

def render_age_time(v):
    return str(datetime.timedelta(seconds = v))

# 将带时区的 `created_at` 日期时间转换为 Unix 时间戳
def calculate_age(created_at_datetime):
    created_at_ts = datetime.datetime.strptime(created_at_datetime, '%Y-%m-%dT%H:%M:%S.%fZ')
    now = datetime.datetime.now()
    return (now - created_at_ts).total_seconds()

测试存储管理自动化

若要测试存储管理自动化,你可能需要生成测试数据,或填充存储以验证分析和删除功能是否符合预期。以下章节提供了关于测试和在短时间内生成存储 blob 的工具与技巧。

生成作业工件

创建一个测试项目,使用 CI/CD 作业矩阵构建生成模拟工件 blob。添加 CI/CD 流水线以每日生成工件。

  1. 创建一个新项目。

  2. 将以下片段添加到 .gitlab-ci.yml 中,包含作业工件生成器配置。

    include:
        - remote: https://gitlab.com/gitlab-da/use-cases/efficiency/job-artifact-generator/-/raw/main/.gitlab-ci.yml
  3. 配置流水线调度

  4. 手动触发流水线

或者,在 MB_COUNT 变量中将每日生成的 86 MB 减少为不同值。

include:
    - remote: https://gitlab.com/gitlab-da/use-cases/efficiency/job-artifact-generator/-/raw/main/.gitlab-ci.yml

generator:
    parallel:
        matrix:
            - MB_COUNT: [1, 5, 10, 20, 50]

有关更多信息,请参阅 Job Artifact Generator README,其中包含一个 示例组

带有过期时间的作业工件生成

项目的 CI/CD 配置指定了作业定义的位置:

  • .gitlab-ci.yml 配置文件中。
  • artifacts:expire_in 设置中。
  • 项目文件和模板中。

为了测试分析脚本,gen-job-artifacts-expiry-included-jobs 项目提供了一个示例配置。


# .gitlab-ci.yml
include:
    - include_jobs.yml

default:
  artifacts:
      paths:
          - '*.txt'

.gen-tmpl:
    script:
        - dd if=/dev/urandom of=${$MB_COUNT}.txt bs=1048576 count=${$MB_COUNT}

generator:
    extends: [.gen-tmpl]
    parallel:
        matrix:
            - MB_COUNT: [1, 5, 10, 20, 50]
    artifacts:
        untracked: false
        when: on_success
        expire_in: 30 days

# include_jobs.yml
.includeme:
    script:
        - dd if=/dev/urandom of=1.txt bs=1048576 count=1

included-job10:
    script:
        - echo "Servus"
        - !reference [.includeme, script]
    artifacts:
        untracked: false
        when: on_success
        expire_in: 10 days

included-job1:
    script:
        - echo "Gruezi"
        - !reference [.includeme, script]
    artifacts:
        untracked: false
        when: on_success
        expire_in: 1 days

included-job30:
    script:
        - echo "Grias di"
        - !reference [.includeme, script]
    artifacts:
        untracked: false
        when: on_success
        expire_in: 30 days

生成容器镜像

示例组 container-package-gen-group 提供的项目可以:

  • 使用 Dockerfile 中的基础镜像构建新镜像。
  • 包含 Docker.gitlab-ci.yml 模板,以便在 GitLab.com SaaS 上构建镜像。
  • 配置流水线调度以每日生成新镜像。

可分叉的示例项目:

生成通用包

示例项目 generic-package-generator 提供的项目可以:

  • 生成随机文本 blob,并使用当前 Unix 时间戳作为发布版本创建 tarball。
  • 使用 Unix 时间戳作为发布版本,将 tarball 上传到通用包注册表。

要生成通用包,可以使用此独立的 .gitlab-ci.yml 配置:

generate-package:
  parallel:
    matrix:
      - MB_COUNT: [1, 5, 10, 20]
  before_script:
    - apt update && apt -y install curl
  script:
    - dd if=/dev/urandom of="${MB_COUNT}.txt" bs=1048576 count=${MB_COUNT}
    - tar czf "generated-$MB_COUNT-nighly-`date +%s`.tar.gz" "${MB_COUNT}.txt"
    - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file "generated-$MB_COUNT-nighly-`date +%s`.tar.gz" "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/generator/`date +%s`/${MB_COUNT}-nightly.tar.gz"'

  artifacts:
    paths:
      - '*.tar.gz'

使用分支测试存储用量

使用以下项目测试带有 分支成本因素 的存储用量:

社区资源

以下资源未得到官方支持。在运行可能无法恢复的破坏性清理命令之前,请务必测试脚本和教程。