#持续交付

0 关注者 · 15 帖子

持续交付 (CD) 是一种软件工程方法,团队可以在短周期内生产软件,以确保软件可以随时可靠地发布。它旨在更快、更频繁地构建、测试和发布软件。

InterSystems 官方 Michael Lei · 一月 5, 2025

2024.3 版  InterSystems IRIS® 数据平台、InterSystems IRIS® for Health 和 HealthShare® Health Connect 现已正式发布 (GA)。

版本亮点

在此版本中,您可以期待一系列激动人心的更新,包括:

  1. 数据库和 WIJ 文件的扩展速度大大提高
  2. 能够通过 Visual Trace 重新发送消息
  3. 增强的规则管理器功能
  4. 向量搜索增强功能
  5. 等等。

请通过开发者社区分享您的反馈,与我们共同打造更好的产品。

文档

请访问以下链接了解所有亮点功能的详细信息:

此外,请查看此版本的升级信息

抢先体验计划 (EAP)

目前提供多个 EAP。 请查看此页面并注册您感兴趣的计划。

如何获取软件?

像往常一样,持续交付 (CD) 版本包含适用于所有受支持平台的经典安装包,以及 Docker 容器格式的容器镜像。

经典安装包

安装包可从 WRC 的 InterSystems IRIS、InterSystems IRIS for Health 和 Health Connect持续交付版本页面获取。此外,还可以在评估服务网站获取套件。

可用性和软件包信息

此版本提供适用于所有受支持平台的经典安装包,以及 Docker 容器格式的容器镜像。有关完整列表,请参阅“支持的平台”文档

安装包和预览版密钥可从 WRC 的预览版下载网站或通过评估服务网站获取。

此持续交付版本的版本号为:2024.3.0.217.0

容器镜像可通过 InterSystems 容器注册表获取。 容器带有 2024.3latest-cd 标签。

0
0 115
文章 Michael Lei · 九月 27, 2024 11m read

欢迎来到我的 CI/CD 系列的下一个章节,我们将探讨使用 InterSystems 技术和 GitLab 进行软件开发可以采用的几种方式。

今天,我们来谈谈互操作性。

问题

当您有一个有效的互操作性生产时,您有两个独立的流程:一个是处理消息的可以正常运行的生产流程,另一个是更新代码、生产配置和系统默认设置的 CI/CD 流程。

显然,CI/CD 流程会影响互操作性。 但问题是:

  • 更新期间究竟发生了什么?
  • 我们需要做些什么以在更新期间尽可能缩短或消除生产停机时间?

术语

  • 业务主机 (BH) – 互操作性生产的一个可配置元素:业务服务 (BS)、业务流程(BP、BPL)或业务操作 (BO)。
  • 业务主机作业 (Job) – 运行业务主机代码并由互操作性生产管理的 InterSystems IRIS 作业。
  • 生产 – 业务主机的互联集合。
  • 系统默认设置 (SDS) – 特定于安装 InterSystems IRIS 的环境的值。
  • 有效消息 – 当前正在由某个业务主机作业处理的请求。 一个业务主机作业最多只能有一条有效消息。 没有有效消息的业务主机作业处于空闲状态。

发生了什么?

我们从生产生命周期开始。

生产启动

首先,可以启动生产。 每个命名空间只能同时运行一个生产,通常而言(除非您真正知道自己在做什么以及为什么这样做),每个命名空间内只能运行一个生产。 不推荐在一个命名空间中于两个或多个不同的生产之间来回切换。 启动生产会启动生产中定义的所有已启用的业务主机。 某些业务主机启动失败不会影响生产启动。

提示:

  • 可以从系统管理门户或者调用以下代码来启动生产:##class(Ens.Director).StartProduction("ProductionName")
  • 通过实现 OnStart 方法可以在生产启动时(在启动任何业务主机作业之前)执行任意代码
  • 生产启动是一个可审核事件。 您始终可以在审核日志中查看是谁在何时启动的生产。

生产更新

在生产启动后,Ens.Director 会持续监视正在运行的生产。 生产存在两种状态:目标状态,在生产类和系统默认设置中定义;以及_运行状态_ – 当前运行的作业及其创建时应用的设置。 如果所需状态与当前状态相同,则一切正常;但如果有差异,就可以(也应该)更新生产。 通常,您会在系统管理门户的“生产配置”页面上看到一个红色的 Update 按钮。

更新生产意味着尝试使当前生产状态与目标生产状态匹配。

当您运行 ##class(Ens.Director).UpdateProduction(timeout=10, force=0) 以更新生产时,它会为每个业务主机执行以下操作:

  1. 将有效设置与生产/SDS/类设置进行比较
  2. 当且仅当 (1) 显示不匹配时,业务主机会被标记为过时并需要更新。

为每个业务主机运行此操作后,UpdateProduction 会构建一组更改:

  • 要停止的业务主机
  • 要启动的业务主机
  • 要更新的生产设置

然后,应用这些更改。

通过这种方式,“更新”设置而不更改任何内容不会导致生产停机。

提示:

  • 可以从系统管理门户或者调用以下代码来更新生产:##class(Ens.Director).UpdateProduction(timeout=10, force=0)
  • 默认的系统管理门户更新超时为 10 秒。 如果您知道处理消息需要更长时间,请调用 Ens.Director:UpdateProduction 并设置更长的超时。
  • 更新超时是一个生产设置,您可以将其更改为更大的值。 此设置适用于系统管理门户。

代码更新

UpdateProduction 不会使用过时的代码更新 BH。 这是一种以安全为导向的行为,但如果您想要在底层代码更改时自动更新所有正在运行的 BH,请按以下步骤操作:

首先,按以下方式加载和编译:

do $system.OBJ.LoadDir(dir, "", .err, 1, .load)
do $system.OBJ.CompileList(load, "curk", .errCompile, .listCompiled)

现在,listCompiled 将包含由于 u 标志而实际编译的所有条目(使用 git 差异来尽可能减小加载集)。 使用此 listCompiled 获取所有已编译类的 $lb:

set classList = ""
set class = $o(listCompiled(""))
while class'="" { 
  set classList = classList _ $lb($p(class, ".", 1, *-1))
  set class=$o(listCompiled(class))
}

然后,计算需要重启的 BH 的列表:

SELECT %DLIST(Name) bhList
FROM Ens_Config.Item 
WHERE 1=1
  AND Enabled = 1
  AND Production = :production
  AND ClassName %INLIST :classList

最后,在获取 bhList 后,停止并启动受影响的主机:

for stop = 1, 0 {
  for i=1:1:$ll(bhList) {
    set host = $lg(bhList, i)
    set sc = ##class(Ens.Director).TempStopConfigItem(host, stop, 0)
  }
  set sc = ##class(Ens.Director).UpdateProduction()
}

生产停止

生产可以停止,这意味着向所有业务主机作业发送关闭请求(如果存在,则在它们处理完有效消息后安全地关闭)。

提示:

  • 可以从系统管理门户或者调用以下代码来停止生产:##class(Ens.Director).StopProduction(timeout=10, force=0)
  • 默认的系统管理门户停止超时为 120 秒。 如果您知道处理消息需要更长时间,请调用 Ens.Director:StopProduction 并设置更长的超时。
  • 关闭超时是一个生产设置。 您可以将其更改为更大的值。 此设置适用于系统管理门户。
  • 通过实现 OnStop 方法可以在生产停止时执行任意代码
  • 生产停止是一个可审核事件,您始终可以在审核日志中查看是谁在何时停止的生产。

重要的一点是,生产是业务主机的总和:

  • 启动生产意味着启动所有已启用的业务主机。
  • 停止生产意味着停止所有正在运行的业务主机。
  • 更新生产意味着计算出过时的业务主机的子集,因此首先停止它们,然后立即重新启动。 此外,新增的业务主机只会启动,从生产中删除的业务主机只会停止。

这会将我们带到业务主机的生命周期。

业务主机启动

业务主机由相同的业务主机作业组成(根据池大小设置的值)。 启动业务主机意味着启动所有业务主机作业。 它们会并行启动。

单个业务主机作业的启动方式如下:

  1. 互操作性作业是一个将成为业务主机作业的新进程。
  2. 新进程注册为互操作性作业。
  3. 业务主机代码和适配器代码加载到进程内存中。
  4. 与业务主机和适配器相关的设置加载到内存中。 优先级顺序如下: a. 生产设置(覆盖系统默认设置和类设置)。 b. 系统默认设置(覆盖类设置)。 c. 类设置。
  5. 作业准备就绪并开始接受消息。

在完成 (4) 后,作业无法更改设置或代码,因此当您导入新的/相同的代码和新的/相同的系统默认设置时,它不会影响当前正在运行的互操作性作业。

业务主机停止

停止业务主机作业意味着:

  1. 互操作性命令作业停止接受更多消息/输入。
  2. 如果存在有效消息,业务主机作业具有一定的超时秒数来处理该消息(完成消息会结束 BO 的 OnMessage 方法,BS 的 OnProcessInput,BPL BP 的状态 S<int> 方法以及 BP 的 On* 方法)。
  3. 如果在超时前有效消息未被处理且 force=0,则生产更新会在该业务主机上失败(您会在 SMP 中看到一个红色的 Update 按钮)。
  4. 如果此列表中的任何一项为真,则表示停止成功:
    • 没有有效消息
    • 有效消息在 timeout 之前处理完
    • 有效消息在超时之前未处理完但 force=1
  5. 作业的互操作性取消注册并停止。

业务主机更新

业务主机更新意味着停止当前运行的业务主机作业并启动新作业。

业务规则、路由规则和 DTL

所有业务主机会在新版本的业务规则、路由规则和 DTL 可用时立即开始使用它们。 在这种情况下,不需要重启业务主机。

离线更新

不过,有时生产更新需要单个业务主机停机。

规则取决于新代码

考虑接下来的情况。 您有一个当前的路由规则 X,它根据任意标准将消息路由到业务流程 A 或 B。 在新提交中,您同时添加:

  • 业务流程 C
  • 新版本的路由规则 X,可以将消息路由到 A、B 或 C。

在此场景下,您不能先加载规则,然后再更新生产。 原因在于,新编译的规则会立即开始将消息路由到业务流程 C,而 InterSystems IRIS 可能尚未编译该规则,或者互操作性尚未更新以供使用。 在这种情况下,您需要禁用包含路由规则的业务主机,更新代码,更新生产,然后再次启用业务主机。

注:

  • 如果您使用生产部署文件更新生产,它会自动禁用/启用所有受影响的 BH。
  • 对于 InProc 调用的主机,编译会使调用方持有的特定主机的缓存失效。

业务主机之间的依赖关系

业务主机之间的依赖关系至关重要。 假设您有业务流程 A 和 B,其中 A 向 B 发送消息。 在新提交中,您同时添加:

  • 新版本的流程 A,可以在向 B 发送的请求中设置新属性 X
  • 新版本的流程 B,可以处理新属性 X

在此场景中,我们必须首先更新流程 B,然后更新流程 A。 您可通过以下两种方式之一完成此操作:

  • 在更新期间禁用业务主机
  • 将更新拆分为两步:首先,仅更新流程 B,然后在单独的更新中开始从流程 A 向流程 B 发送消息。

这个主题一个更具挑战性的变体是,新版本的流程 A 和流程 B 与旧版本不兼容,这需要业务主机停机。

队列

如果您知道更新后某个业务主机将无法处理旧消息,则需要确保在更新前该业务主机队列为空。 为此,请禁用所有向该业务主机发送消息的业务主机,并等到其队列变空。

BPL 业务流程中的状态更改

首先,简单介绍一下 BPL BP 的运作方式。 在您编译 BPL BP 后,会在与完整 BPL 类同名的软件包中创建两个类:

  • Thread1 类包含方法 S1、S2、... SN,对应于 BPL 中的活动
  • Context 类包含所有上下文变量,以及 BPL 将执行的下一个状态(即 S5

此外,BPL 类是持久类,可以存储当前正在处理的请求。

BPL 的工作方式是在 Thread 类中执行 S 方法并相应地更新 BPL 类表、Context 表和 Thread1 表,其中一条“正在处理”的消息是 BPL 表中的一行。 请求处理完后,BPL 会删除 BPL、ContextThread 条目。 由于 BPL BP 是异步的,通过在 S 调用之间保存信息并在不同请求之间切换,一个 BPL 作业可同时处理多个请求。 例如,BPL 处理一个请求,直至其到达 sync 活动 – 等待来自 BO 的回答。 它会将当前上下文保存到磁盘中,同时将 %NextState 属性(位于 Thread1 类中)设置为响应活动 S 方法,并在 BO 回答前继续处理其他请求。 BO 回答后,BPL 会将上下文加载到内存中,并执行与 %NextState 属性中保存的状态对应的方法。

现在,当我们更新 BPL 时会发生什么? 首先,我们需要检查是否至少满足以下两个条件之一:

  • 更新期间,上下文表为空,这意味着没有正在处理的有效消息。
  • 新状态与旧状态相同,或者新状态是在旧状态之后添加的。

如果至少满足一个条件,我们就可以开始更新。 要么没有需要更新后 BPL 处理的更新前请求,要么状态是在结束时添加的,这意味着旧请求也可以进入其中(假设更新前请求与更新后 BPL 活动和处理兼容)。

但是,如果您有正在处理的有效请求,而 BPL 更改了状态顺序,该怎么办? 理想情况下,如果您可以等待,则禁用 BPL 调用方并等待队列为空。 验证上下文表是否也为空。 请记住,队列只显示未处理的请求,而上下文表存储正在处理的请求,因此您可能会遇到一个非常繁忙的 BPL 显示零队列大小的情况,这是正常的。 之后,禁用 BPL,执行更新并启用所有之前禁用的业务主机。

如果无法实现(通常是在 BPL 非常长的情况下,例如,我记得我更新过一个花了大约一周时间处理请求的 BPL,或者更新窗口太短),请使用 BPL 版本控制

或者,您也可以编写一个更新脚本。 在此更新脚本中,将旧的后续状态映射到新的后续状态并在 Thread1 表上运行,以便更新的 BPL 可以处理旧请求。 当然,在更新期间必须禁用 BPL。 也就是说,这是一种极为罕见的情况,通常您不必这样做,但如果您需要这样做,方法就是如此。

结论

为了将底层代码更改后实现生产所需的操作数降到最低,互操作性实现了一种复杂的算法。 每次 SDS 更新时,调用具有安全超时的 UpdateProduction。 对于每次代码更新,您都需要决定一种更新策略。

通过使用 git 差异减少编译的代码量有助于缩短编译时间,但利用自身“更新”代码并重新编译它或使用相同的值“更新”设置不会触发或要求进行生产更新。

更新和编译业务规则、路由规则和 DTL 可使它们在未进行生产更新的情况下立即可用。

最后,生产更新是一项安全操作,通常不需要停机。

链接

在本文撰写期间,@James MacKeith@Dmitry Zasypkin@Regilo Regilio Guedes de Souza 提供了宝贵的帮助,作者对此深表感谢。

0
0 117
文章 Michael Lei · 九月 27, 2024 7m read

经过将近四年的停顿,我的 CI/CD 系列又回来了! 这些年来,我与多个 InterSystems 客户合作,为不同的用例开发 CI/CD 管道。 希望本文中提供的信息对您有所帮助。

系列文章探讨了使用 InterSystems 技术和 GitLab 进行软件开发可以采用的几种方式。

我们有一系列激动人心的话题要讨论:但今天,我们来谈谈代码之外的事情 – 配置和数据。

问题

之前我们探讨过代码提升,这在某种程度上是无状态的 – 我们总是从一个(大概)空实例到完整的代码库。 但有时,我们需要提供数据或状态。 不同的数据类型包括:

  • 配置:用户、Web 应用、LUT、自定义架构、任务、业务伙伴等
  • 设置:环境特定的键值对
  • 数据:为了让应用正常工作,通常必须提供引用表等

我们来探讨所有这些数据类型,以及如何先将它们提交到源代码控制工具中,然后进行部署。

配置

系统配置分布在许多不同的类中,但 InterSystems IRIS 可以将大多数类导出为 XML。 首先是一个安全软件包,其中包含以下信息:

  • Web 应用程序
  • DocDB
  • 审核事件
  • KMIP 服务器
  • LDAP 配置
  • 资源
  • 角色
  • SQL 特权
  • SSL 配置
  • 服务
  • 用户

所有这些类都提供 Exists、Export 和 Import 方法,允许您在不同的环境之间移动它们。

一些注意事项:

  • 用户和 SSL 配置可能包含敏感信息,例如密码。 出于安全原因,通常建议不要将它们存储在源代码控制工具中。 使用 Export/Import 方法来方便地进行一次性传输。
  • 默认情况下,Export/Import 方法会将所有内容输出到一个文件中,这可能不适合源代码控制。 这里提供了一个实用类,可用于导出和导入查找表、自定义架构、业务伙伴、任务、凭据和 SSL 配置。 它会每个文件导出一个条目,因此您会得到一个包含 LUT 的目录,另一个包含自定义架构的目录,等等。 对于 SSL 配置,它还会导出以下文件:证书和密钥。

另外值得注意的是,除了导出/导入,您还可以使用 %InstallerMerge CPF 来创建这些条目中的大多数。 这两个工具还支持创建命名空间和数据库。 Merge CPF 可以调整系统设置,例如全局缓冲区大小。

任务

%SYS.Task 类存储任务并提供 ExportTasksImportTasks 方法。 您还可以查看上面的实用类,以逐个导入和导出任务。 请注意,当您导入任务时,如果 StartDate 或其他与计划相关的属性在过去的时间里,则可能会遇到导入错误 (ERROR #7432: Start Date and Time must be after the current date and time)。 解决方案是将 LastSchedule 设置为 0,InterSystems IRIS 会将新导入的任务重新安排为在不久的将来运行。

互操作性

互操作性生产包含:

  • 业务伙伴
  • 系统默认设置
  • 凭据
  • 查找表

前两个可以在 Ens.Config 软件包中通过 %Export%Import 方法获取。 使用上面的实用类导出凭据和查找表。 在最近的版本中,查找表可以通过 $system.OBJ 类导出/导入。

设置

系统默认设置 – 是环境特定设置的默认互操作性机制:

系统默认设置的目的是简化将生产定义从一个环境复制到另一个环境的过程。 在任何生产中,一些设置的值作为生产设计的一部分确定;通常而言,这些设置在所有环境中都应相同。 不过,其他设置必须根据环境进行调整;这些设置包括文件路径、端口号等。

系统默认设置应仅指定特定于安装 InterSystems IRIS 的环境的值。 相比之下,生产定义应指定在所有环境中都应当相同的设置值。

我强烈建议在生产环境中使用它们。 使用 %Export%Import 传输系统默认设置。

应用程序设置

您的应用程序可能也使用设置。 在这种情况下,我建议使用系统默认设置。 虽然这是一种互操作性机制,但可以通过以下方式访问设置:%GetSetting(pProductionName, pItemName, pHostClassName, pTargetType, pSettingName, Output pValue)文档)。 您可以编写一个包装器来设置您不关心的默认值,例如:

ClassMethod GetSetting(name, Output value) As %Boolean [Codemode=expression]
{
##class(Ens.Config.DefaultSettings).%GetSetting("myAppName", "default", "default", , name, .value)
}

如果您想要更多类别,还可以公开 pItemName 和/或 pHostClassName 参数。 可以通过导入、使用系统管理门户、创建 Ens.Config.DefaultSettings 类的对象或设置 ^Ens.Config.DefaultSettingsD 全局进行初始设置。

我的主要建议是将设置放在一个地方(可以是系统默认设置或自定义解决方案),应用程序必须仅通过提供的 API 获取设置。 这样,应用程序本身不知道环境,剩下的工作是为集中式设置存储提供环境特定的值。 为此,在您的仓库中创建一个包含设置文件的设置文件夹,文件名称与环境分支名称相同。 随后在 CI/CD 阶段,使用 $CI_COMMIT_BRANCH环境变量加载正确的文件。

DEV.xml
TEST.xml
PROD.xml

如果每个环境有多个设置文件,请使用以环境分支命名的文件夹。 要从 InterSystems IRIS 内获取环境变量值,请使用$System.Util.GetEnviron("name")

数据

如果您想让一些数据(引用表、目录等)可用,可以通过几种方式实现此目标:

  • Global导出。 使用二进制 GOF 导出或新的 XML 导出。 使用 GOF 导出时,请记住源系统和目标系统上的区域设置必须匹配(或者至少目标系统上的全局排序规则必须可用)。 XML 导出会占用更多空间。 您可以通过将Global导出为 xml.gz 文件进行改进,$system.OBJ 方法会根据需要自动(取消)归档 xml.gz 文件。 这种方式的主要缺点是数据无法被人类阅读,即使是 XML 也是如此 – 大部分采用 base64 编码。
  • CSV。 导出 CSV 并通过 LOAD DATA 导入。 我更喜欢使用 CSV,因为它是最节省存储空间的人类可读格式,并且任何内容都可以导入。
  • JSON。 使类支持 JSON
  • XML。 使类支持 XML 以将对象投射到 XML 中。 如果您的数据具有复杂结构,请使用该方式。

选择哪种格式取决于您的用例。 这里我按存储效率顺序列出了各个格式,但如果您没有大量数据,那就不必担心。

结论

状态为您的 CI/CD 部署管道增加了额外的复杂性,但 InterSystems IRIS 为此提供了大量管理工具。

链接

0
0 85
文章 Michael Lei · 九月 27, 2024 6m read

在这一系列文章中,我想向大家介绍并探讨使用 InterSystems 技术和 GitLab 进行软件开发可以采用的几种方式。 我将介绍以下主题:

  • Git 101
  • Git 流程(开发流程)
  • GitLab 安装
  • GitLab 工作流
  • 持续交付
  • GitLab 安装和配置
  • GitLab CI/CD
  • 为何使用容器?
  • 容器基础架构
  • 使用容器的 CD
  • 使用 ICM 的 CD
  • 容器架构

在本文中,我们将讨论如何构建并部署您自己的容器。

Durable %SYS

由于容器是临时的,它们不应存储任何应用程序数据。 持久化 %SYS 功能让我们能够实现这一点 – 将设置、配置、%SYS 数据等存储到主机卷上,即:

  • iris.cpf 文件。
  • /csp 目录,包含 Web 网关配置和日志文件。
  • /httpd/httpd.conf 文件,实例的私有 Web 服务器的配置文件。
  • /mgr 目录,包含以下内容:
    • IRISSYS 系统数据库,包括 IRIS.DAT 和 iris.lck 文件、流目录,以及包含 IRISTEMP、IRISAUDIT、IRIS 和 USER 系统数据库的 iristemp、irisaudit、iris 和 user 目录。
    • 写入镜像日志文件,IRIS.WIJ。
    • /journal 目录,包含日志文件。
    • /temp 目录,用于存储临时文件。
    • 日志文件,包括 messages.log、journal.log 和 SystemMonitor.log。

容器架构

另一方面,我们需要将应用程序代码存储在我们的容器内,以便在需要时进行升级。

这一切使我们实现了如下所示的架构:

为了在构建过程中实现这一点,我们至少需要创建一个额外的数据库(用于存储应用程序代码)并将其映射到我们的应用程序命名空间。 在我的示例中,我将使用 USER 命名空间来保存应用程序数据,因为它已经存在且是持久化的。

安装程序

基于上述内容,我们的安装程序需要执行以下任务:

  • 创建 APP 命名空间/数据库
  • 将代码加载到 APP 命名空间
  • 将我们的应用程序类映射到 USER 命名空间
  • 完成所有其他安装(在本例中,我创建了 CSP Web 应用和 REST 应用)
Class MyApp.Hooks.Local
{

Parameter Namespace = "APP";

/// See generated code in zsetup+1^MyApp.Hooks.Local.1 XData Install [ XMLNamespace = INSTALLER ] { <Manifest>

<Log Text="Creating namespace ${Namespace}" Level="0"/> <Namespace Name="${Namespace}" Create="yes" Code="${Namespace}" Ensemble="" Data="IRISTEMP"> <Configuration> <Database Name="${Namespace}" Dir="/usr/irissys/mgr/${Namespace}" Create="yes" MountRequired="true" Resource="%DB_${Namespace}" PublicPermissions="RW" MountAtStartup="true"/> </Configuration>

<Import File="${Dir}Form" Recurse="1" Flags="cdk" IgnoreErrors="1" /> </Namespace> <Log Text="End Creating namespace ${Namespace}" Level="0"/>

<Log Text="Mapping to USER" Level="0"/> <Namespace Name="USER" Create="no" Code="USER" Data="USER" Ensemble="0"> <Configuration> <Log Text="Mapping Form package to USER namespace" Level="0"/> <ClassMapping From="${Namespace}" Package="Form"/> <RoutineMapping From="${Namespace}" Routines="Form" /> </Configuration>

<CSPApplication Url="/" Directory="${Dir}client" AuthenticationMethods="64" IsNamespaceDefault="false" Grant="%ALL" Recurse="1" /> </Namespace>

</Manifest> }

/// This is a method generator whose code is generated by XGL. /// Main setup method /// set vars("Namespace")="TEMP3" /// do ##class(MyApp.Hooks.Global).setup(.vars) ClassMethod setup(ByRef pVars, pLogLevel As %Integer = 0, pInstaller As %Installer.Installer) As %Status [ CodeMode = objectgenerator, Internal ] { Quit ##class(%Installer.Manifest).%Generate(%compiledclass, %code, "Install") }

/// Entry point ClassMethod onAfter() As %Status { try { write "START INSTALLER",! set vars("Namespace") = ..#Namespace set vars("Dir") = ..getDir() set sc = ..setup(.vars) write !,$System.Status.GetErrorText(sc),!

    set sc = ..createWebApp()
} catch ex {
    set sc = ex.AsStatus()
    write !,$System.Status.GetErrorText(sc),!
}
quit sc

}

/// Modify web app REST ClassMethod createWebApp(appName As %String = "/forms") As %Status { set:$e(appName)'="/" appName = "/" _ appName #dim sc As %Status = $$$OK new $namespace set $namespace = "%SYS" if '##class(Security.Applications).Exists(appName) { set props("AutheEnabled") = $$$AutheUnauthenticated set props("NameSpace") = "USER" set props("IsNameSpaceDefault") = $$$NO set props("DispatchClass") = "Form.REST.Main" set props("MatchRoles")=":" _ $$$AllRoleName set sc = ##class(Security.Applications).Create(appName, .props) } quit sc }

ClassMethod getDir() [ CodeMode = expression ] { ##class(%File).NormalizeDirectory($system.Util.GetEnviron("CI_PROJECT_DIR")) }

}

为了创建非持久化数据库,我会使用 /usr/irissys/mgr 的一个子目录,它不是持久的。 请注意,调用 ##class(%File).ManagerDirectory() 会返回持久化目录的路径,而不是内部容器目录的路径。

 

持续交付配置

查看第 7 部分以了解完整信息,但我们需要做的就是在现有配置中添加这两行(加粗)代码。

run image:
  stage: run
  environment:
    name: $CI_COMMIT_REF_NAME
    url: http://$CI_COMMIT_REF_SLUG.docker.eduard.win/index.html
  tags:
    - test
  script:
    - docker run -d
      --expose 52773
      --volume /InterSystems/durable/$CI_COMMIT_REF_SLUG:/data
      --env ISC_DATA_DIRECTORY=/data/sys
      --env VIRTUAL_HOST=$CI_COMMIT_REF_SLUG.docker.eduard.win
      --name iris-$CI_COMMIT_REF_NAME
      docker.eduard.win/test/docker:$CI_COMMIT_REF_NAME
      --log $ISC_PACKAGE_INSTALLDIR/mgr/messages.log

volume 参数将主机目录挂载到容器中,ISC_DATA_DIRECTORY 变量告诉 InterSystems IRIS 使用哪个目录。 引用文档内容:

当您使用这些选项运行 InterSystems IRIS 容器时,会发生以下情况:
  • 指定的外部卷被挂载。
  • 如果由 ISC_DATA_DIRECTORY 环境变量指定的持久化 %SYS 目录(上述示例中的 iconfig/)已经存在且包含持久化 %SYS 数据,则实例的所有内部指针都会重置到该目录,实例使用其中包含的数据。
  • 如果 ISC_DATA_DIRECTORY 环境变量指定的持久化 %SYS 目录已经存在但不包含持久化 %SYS 数据,则不会复制数据,实例使用容器内安装树中的数据运行,这意味着实例特定的数据不是持久的。 因此,您可能需要在脚本中包含对此条件的检查,然后再运行容器。
  • 如果 ISC_DATA_DIRECTORY 指定的持久化 %SYS 目录不存在:
    • 指定的持久化 %SYS 目录已创建。
    • 将持久化 %SYS 目录内容中列出的目录和文件从其安装位置复制到持久化 %SYS 目录(原始文件仍留在原位)。
    • 实例的所有内部指针都会重置到持久化 %SYS 目录,实例使用其中包含的数据。

 

更新

当应用程序演进并发布新版本(容器)时,有时您可能需要运行一些代码。 这可能是预编译/后编译挂钩、架构迁移、单元测试,但归根结底,您需要运行任意代码。 这就是为什么您需要一个管理应用程序的框架。 在之前的文章中,我概述了这种框架的基本结构,但它当然可以大幅度扩展以满足特定应用程序的需求。

结论

创建容器化应用程序需要一些思考,但 InterSystems IRIS 提供了几个功能,可以让此流程更加轻松。

0
0 97
文章 Michael Lei · 九月 27, 2024 9m read

在这一系列文章中,我想向大家介绍并探讨使用 InterSystems 技术和 GitLab 进行软件开发可以采用的几种方式。 我将介绍以下主题:

  • Git 101
  • Git 流程(开发流程)
  • GitLab 安装
  • GitLab 工作流
  • 持续交付
  • GitLab 安装和配置
  • GitLab CI/CD
  • 为何使用容器?
  • 容器基础架构
  • 使用容器的 CD
  • 使用 ICM 的 CD

在本文中,我们将使用 InterSystems Cloud Manager 构建持续交付。 ICM 是一个面向基于 InterSystems IRIS 的应用程序的云配置和部署解决方案。 它允许您定义所需部署配置,ICM 会自动提供这些配置。 有关详情,请参阅 ICM 概述

工作流

在我们的持续交付配置中,我们将完成以下任务:

  • 将代码推送到 GitLab 仓库
  • 构建 docker 镜像
  • 将镜像发布到 docker 注册表
  • 在测试服务器上测试镜像
  • 如果测试通过,则在生产服务器上部署

也可以用示意图形式表示此流程:

如您所见,基本操作是相同的,不过我们将使用 ICM 而不是手动方式来管理 Docker 容器。

ICM 配置

在我们开始升级容器之前,需要对它们进行配置。 因此,我们需要定义 defaults.jsondefinitions.json 来描述我们的架构。 我将为 LIVE 服务器提供这 2 个文件,TEST 服务器的 definitions 是相同的,defaults 只在 TagSystemMode 值上有所不同。

defaults.json:

{
    "Provider": "GCP",
    "Label": "gsdemo2",
    "Tag": "LIVE","SystemMode": "LIVE",
    "DataVolumeSize": "10",
    "SSHUser": "sample",
    "SSHPublicKey": "/icmdata/ssh/insecure.pub",
    "SSHPrivateKey": "/icmdata/ssh/insecure",
    "DockerImage": "eduard93/icmdemo:master",
    "DockerUsername": "eduard93",
    "DockerPassword": "...",
    "TLSKeyDir": "/icmdata/tls",
    "Credentials": "/icmdata/gcp.json",
    "Project": "elebedyu-test",
    "MachineType": "n1-standard-1",
    "Region": "us-east1",
    "Zone": "us-east1-b",
    "Image": "rhel-cloud/rhel-7-v20170719",
    "ISCPassword": "SYS",
    "Mirror": "false"
}

definitions.json

[
    {
    "Role": "DM",
    "Count": "1",
    "ISCLicense": "/icmdata/iris.key"
    }
]

在 ICM 容器内部,/icmdata 文件夹从主机挂载,并且:

  • TEST 服务器的定义放置在 /icmdata/test 文件夹中
  • LIVE 服务器的定义放置在 /icmdata/live 文件夹中

获取所有必需的密钥后:

keygenSSH.sh /icmdata/ssh
keygenTLS.sh /icmdata/tls

并将必需的文件放置在 /icmdata 中:

  • iris.key
  • gcp.json(用于部署到 Google Cloud Platform)

调用 ICM 来配置您的实例:

cd /icmdata/test
icm provision
icm run
cd /icmdata/live
icm provision
icm run

这将为每个 TEST 和 LIVE 服务器配置一个独立的 InterSystems IRIS 实例。

有关更详细的指南,请参阅 ICM 概述

构建

首先,我们需要构建镜像。

我们的代码通常存储在仓库中,CD 配置位于 gitlab-ci.yml 中,但为了提高安全性,我们会在构建服务器上存储几个服务器特定的文件。

iris.key

许可证密钥。 或者,它可以在容器构建过程中下载,而不是存储在服务器上。 将密钥存储在仓库中非常不安全。

pwd.txt

包含默认密码的文件。 将这类文件存储在仓库中也非常不安全。 此外,如果您在单独的服务器上托管生产环境,它可能有不同的默认密码。

load_ci_icm.script

初始脚本,它执行以下任务:

  • 加载安装程序
  • 安装程序执行应用程序初始化
  • 加载代码
set dir = ##class(%File).NormalizeDirectory($system.Util.GetEnviron("CI_PROJECT_DIR"))
do ##class(%SYSTEM.OBJ).Load(dir _ "Installer/Global.cls","cdk")
do ##class(Installer.Global).init()
halt

请注意,第一行有意留空。

与之前的示例相比,有几个不同之处。 首先,我们没有启用操作系统身份验证,因为 ICM 会与容器交互而不是直接与 GitLab 交互。 其次,我使用安装程序清单来初始化我们的应用程序,以展示不同的初始化方式。 有关安装程序的更多信息,请参阅这篇文章。 最后,我们将在 Docker Hub 中以私有仓库的形式发布我们的镜像。

 

Installer/Global.cls

我们的安装程序清单如下所示:

<Manifest>
    <Log Text="Creating namespace ${Namespace}" Level="0"/>
    <Namespace Name="${Namespace}" Create="yes" Code="${Namespace}" Ensemble="" Data="IRISTEMP">
        <Configuration>
            <Database Name="${Namespace}" Dir="${MGRDIR}/${Namespace}" Create="yes" MountRequired="true" Resource="%DB_${Namespace}" PublicPermissions="RW" MountAtStartup="true"/>
        </Configuration>

        <Import File="${Dir}MyApp" Recurse="1" Flags="cdk" IgnoreErrors="1" />
    </Namespace>

    <Log Text="Mapping to USER" Level="0"/>
    <Namespace Name="USER" Create="no" Code="USER" Data="USER" Ensemble="0">
        <Configuration>
            <Log Text="Mapping MyApp package to USER namespace" Level="0"/>
            <ClassMapping From="${Namespace}" Package="MyApp"/>
        </Configuration>

        <CSPApplication  Url="/"      Directory="${Dir}client" AuthenticationMethods="64" IsNamespaceDefault="false" Grant="%ALL"  />
        <CSPApplication  Url="/myApp" Directory="${Dir}"       AuthenticationMethods="64" IsNamespaceDefault="false" Grant="%ALL"  />
    </Namespace>
</Manifest>

它实现了以下更改:

  1. 创建应用程序命名空间。
  2. 创建应用程序代码数据库(数据将存储在 USER 数据库中)。
  3. 将代码加载到应用程序代码数据库中。
  4. 将 MyApp 软件包映射到 USER 命名空间。
  5. 创建 2 个 Web 应用程序:分别用于 HTML 和 REST。

gitlab-ci.yml

现在,继续持续交付配置:

build image:
  stage: build
  tags:
    - master
  script:
    - cp -r /InterSystems/mount ci
    - cd ci
    - echo 'SuperUser' | cat - pwd.txt load_ci_icm.script > temp.txt
    - mv temp.txt load_ci.script
    - cd ..
    - docker build --build-arg CI_PROJECT_DIR=$CI_PROJECT_DIR -t eduard93/icmdemo:$CI_COMMIT_REF_NAME .

这里会执行哪些操作?

首先,由于 docker build 只能访问基础构建目录(在我们的示例中为仓库根目录)的子目录,我们需要将我们的“秘密”目录(其中包含 iris.keypwd.txtload_ci_icm.script)复制到克隆的仓库中。

接下来,首次终端访问需要用户名/密码,因此我们会将这些信息添加到 load_ci.script 中(这也是 load_ci.script 开头一行留空的原因)。

最后,我们会构建 docker 镜像并适当地为其添加标签:eduard93/icmdemo:$CI_COMMIT_REF_NAME

其中,$CI_COMMIT_REF_NAME 是当前分支的名称。 请注意,镜像标签的第一部分应与 GitLab 中的项目名称相同,这样才能在 GitLab 的“注册表”标签页中看到它(“注册表”标签页中提供了关于添加标签的说明)。

Dockerfile

构建 docker 镜像是通过 Dockerfile 完成的,具体如下:

FROM intersystems/iris:2018.1.1-released

ENV SRC_DIR=/tmp/src
ENV CI_DIR=$SRC_DIR/ci
ENV CI_PROJECT_DIR=$SRC_DIR

COPY ./ $SRC_DIR

RUN cp $CI_DIR/iris.key $ISC_PACKAGE_INSTALLDIR/mgr/ \
 && cp $CI_DIR/GitLab.xml $ISC_PACKAGE_INSTALLDIR/mgr/ \
 && $ISC_PACKAGE_INSTALLDIR/dev/Cloud/ICM/changePassword.sh $CI_DIR/pwd.txt \
 && iris start $ISC_PACKAGE_INSTANCENAME \
 && irissession $ISC_PACKAGE_INSTANCENAME -U%SYS < $CI_DIR/load_ci.script \
 && iris stop $ISC_PACKAGE_INSTANCENAME quietly

我们从基本的 iris 容器开始。

首先,我们将仓库(和“秘密”目录)复制到容器中。

接下来,我们将许可证密钥复制到 mgr 目录中。

然后,我们将密码更改为 pwd.txt 中的值。 请注意,此操作会删除 pwd.txt。

之后,实例启动并执行 load_ci.script。

最后,iris 实例停止。

请注意,我使用的是 GitLab Shell 执行器,而不是 Docker 执行器。 当您需要从镜像内部提取某些内容时,将使用 Docker 执行器,例如在 Java 容器中构建 Android 应用程序并且只需要一个 apk 时。 在我们的示例中,我们需要整个容器,因此需要使用 Shell 执行器。 因此,我们通过 GitLab Shell 执行器运行 Docker 命令。

发布

现在,我们将镜像发布到 Docker Hub

publish image:
  stage: publish
  tags:
    - master
  script:
    - docker login -u eduard93 -p ${DOCKERPASSWORD}
    - docker push eduard93/icmdemo:$CI_COMMIT_REF_NAME

注意 ${DOCKERPASSWORD} 变量,它是 GitLab 的秘密变量。 我们可以在“GitLab > 项目 > 设置 > CI/CD > 变量”中添加它们:

作业日志中也不包含密码值:

Running with gitlab-runner 10.6.0 (a3543a27)
  on icm 82634fd1
Using Shell executor...
Running on docker...
Fetching changes...
Removing ci/
HEAD is now at 8e24591 Add deploy to LIVE
Checking out 8e245910 as master...Skipping Git submodules setup$ docker login -u eduard93 -p ${DOCKERPASSWORD}
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded
$ docker push eduard93/icmdemo:$CI_COMMIT_REF_NAME
The push refers to repository [docker.io/eduard93/icmdemo]
master: digest: sha256:d1612811c11154e77c84f0c08a564a3edeb7ddbbd9b7acb80754fda97f95d101 size: 2620
Job succeeded

在 Docker Hub 中,我们可以看到新镜像:

 

运行

我们有了镜像,接下来在我们的测试服务器上运行它。 脚本如下。

run image:
  stage: run
  environment:
    name: $CI_COMMIT_REF_NAME
  tags:
    - master
  script:
    - docker exec icm sh -c "cd /icmdata/test && icm upgrade -image eduard93/icmdemo:$CI_COMMIT_REF_NAME"

使用 ICM,我们只需要运行一个命令 (icm upgrade) 即可升级现有部署。 我们通过运行“docker exec icm sh -c”来调用它,这会在 icm 容器内执行指定的命令。首先,我们进入 /icmdata/test,这里定义了我们为 TEST 服务器准备的 ICM 部署定义。 之后,我们调用 icm upgrade 将当前存在的容器替换为新容器。

测试

我们来运行一些测试。

test image:
  stage: test
  tags:
    - master
  script:
    - docker exec icm sh -c "cd /icmdata/test && icm session -namespace USER -command 'do \$classmethod(\"%UnitTest.Manager\",\"RunTest\",\"MyApp/Tests\",\"/nodelete\")' | tee /dev/stderr | grep 'All PASSED' && exit 0 || exit 1"

同样,我们在 icm 容器内执行一条命令。 icm 会话在一个已部署的节点上执行命令。 该命令运行单元测试。 之后,它将所有输出传输到屏幕,同时也传输给 grep 以查找单元测试结果,并成功退出进程或以出错方式退出。

部署

生产服务器上的部署与测试服务器上的部署完全相同,只是为 LIVE 部署定义使用了另一个目录。 如果测试失败,此阶段将不会被执行。

deploy image:
  stage: deploy
  environment:
    name: $CI_COMMIT_REF_NAME
  tags:
    - master
  script:
    - docker exec icm sh -c "cd /icmdata/live && icm upgrade -image eduard93/icmdemo:$CI_COMMIT_REF_NAME"

结论

ICM 为您提供了一种简单直观的方式来配置云基础架构并在它上面部署服务,帮助您立即进入云,无需大规模开发或重新工具化。 基础架构即代码 (IaC) 和容器化部署的优势使您可以轻松地在公共云平台(如 Google、Amazon 和 Azure)或者在您的私有 VMware vSphere 云上部署基于 InterSystems IRIS 的应用程序。 定义您的需求,发出几条命令,ICM 会完成其余工作。
即使您已经在使用云基础架构、容器或两者,ICM 也能通过自动执行大量原本需要手动完成的步骤,极大地减少配置和部署应用程序所需的时间和精力。
0
0 87
文章 Michael Lei · 九月 27, 2024 9m read

在这一系列文章中,我想向大家介绍并探讨使用 InterSystems 技术和 GitLab 进行软件开发可以采用的几种方式。 我将介绍以下主题:

  • Git 101
  • Git 流程(开发流程)
  • GitLab 安装
  • GitLab 工作流
  • 持续交付
  • GitLab 安装和配置
  • GitLab CI/CD
  • 为何使用容器
  • 容器基础架构
  • 使用容器的 CD

第一篇文章中,我们介绍了 Git 基础知识、深度理解 Git 概念对现代软件开发至关重要的原因,以及如何使用 Git 开发软件。

第二篇文章中,我们介绍了 GitLab 工作流 – 一个完整的软件生命周期流程,并介绍了持续交付。

第三篇文章中,我们介绍了 GitLab 安装和配置以及将环境连接到 GitLab

第四篇文章中,我们编写了 CD 配置。

第五篇文章中,我们讨论了容器以及使用容器的方式(和原因)。

第六篇文章中,我们将探讨运行包含容器的持续交付管道所需的主要组件以及这些组件如何协同运行。

在这篇文章中,我们将构建上一篇文章中探讨的持续交付配置。

工作流

在持续交付配置中,我们会:

  • 将代码推送到 GitLab 仓库
  • 构建 docker 镜像
  • 进行测试
  • 将镜像发布到 docker 注册表
  • 将旧容器换为注册表中的新版本

也可以用示意图形式表示此流程:

我们开始吧。

构建

首先,我们需要构建镜像。

我们的代码通常存储在仓库中,CD 配置位于 gitlab-ci.yml 中,但为了提高安全性,我们会在构建服务器上存储几个服务器特定的文件。

GitLab.xml

包含 CD 挂钩代码。 该代码是在上一篇文章中开发的,并在 GitHub 上提供。 这是一个小型库,用于加载代码、运行各种挂钩以及测试代码。 作为更好的替代方案,您可以使用 git 子模块将此项目或类似项目包含到您的仓库中。 最好选择子模块,因为子模块更容易保持最新状态。 另一个替代方案是在 GitLab 上为版本添加标签,并使用 ADD 命令加载这些版本。

iris.key

许可证密钥。 或者,它可以在容器构建过程中下载,而不是存储在服务器上。 将密钥存储在仓库中非常不安全。

pwd.txt

包含默认密码的文件。 将这类文件存储在仓库中也非常不安全。 此外,如果您在单独的服务器上托管生产环境,它可能有不同的默认密码。

load_ci.script

初始脚本,它执行以下任务:

  • 实现操作系统身份验证
  • 加载 GitLab.xml
  • 初始化 GitLab 实用工具设置
  • 加载代码

set sc = ##Class(Security.System).Get("SYSTEM",.Properties) write:('sc) $System.Status.GetErrorText(sc) set AutheEnabled = Properties("AutheEnabled") set AutheEnabled = $zb(+AutheEnabled,16,7) set Properties("AutheEnabled") = AutheEnabled set sc = ##Class(Security.System).Modify("SYSTEM",.Properties) write:('sc) $System.Status.GetErrorText(sc) zn "USER" do ##class(%SYSTEM.OBJ).Load(##class(%File).ManagerDirectory() _ "GitLab.xml","cdk") do ##class(isc.git.Settings).setSetting("hooks", "MyApp/Hooks/") do ##class(isc.git.Settings).setSetting("tests", "MyApp/Tests/") do ##class(isc.git.GitLab).load() halt

请注意,第一行有意留空。

由于某些设置可以是服务器特定的,脚本不会存储在仓库中,而是单独存储。 如果此初始挂钩始终相同,将其存储在仓库中即可。

gitlab-ci.yml

现在,继续持续交付配置:

构建镜像:
  stage: build
  tags:
    - test
  script:
    - cp -r /InterSystems/mount ci
    - cd ci
    - echo 'SuperUser' | cat - pwd.txt load_ci.script > temp.txt
    - mv temp.txt load_ci.script
    - cd ..
    - docker build --build-arg CI_PROJECT_DIR=$CI_PROJECT_DIR -t docker.domain.com/test/docker:$CI_COMMIT_REF_NAME .

这里会执行哪些操作?

首先,由于 docker build 只能访问基础构建目录的子目录(本例中是仓库根目录),我们需要将“秘密”目录(包含 GitLab.xmliris.keypwd.txt 和 load_ci.script 的目录)复制到克隆的仓库中。

接下来,首次终端访问需要用户名/密码,因此我们会将这些信息添加到 load_ci.script 中(这也是 load_ci.script 开头一行留空的原因)。

最后,我们会构建 docker 镜像并适当地为其添加标签:docker.domain.com/test/docker:$CI_COMMIT_REF_NAME

其中,$CI_COMMIT_REF_NAME 是当前分支的名称。 请注意,镜像标签的第一部分应与 GitLab 中的项目名称相同,这样才能在 GitLab 的“注册表”标签页中看到它(“注册表”标签页中提供了关于添加标签的说明)。

Dockerfile

构建 docker 镜像是通过 Dockerfile 完成的,具体如下:

FROM docker.intersystems.com/intersystems/iris:2018.1.1.611.0

ENV SRC_DIR=/tmp/src ENV CI_DIR=$SRC_DIR/ci ENV CI_PROJECT_DIR=$SRC_DIR

COPY ./ $SRC_DIR

RUN cp $CI_DIR/iris.key $ISC_PACKAGE_INSTALLDIR/mgr/
&& cp $CI_DIR/GitLab.xml $ISC_PACKAGE_INSTALLDIR/mgr/
&& $ISC_PACKAGE_INSTALLDIR/dev/Cloud/ICM/changePassword.sh $CI_DIR/pwd.txt
&& iris start $ISC_PACKAGE_INSTANCENAME
&& irissession $ISC_PACKAGE_INSTANCENAME -U%SYS < $CI_DIR/load_ci.script
&& iris stop $ISC_PACKAGE_INSTANCENAME quietly

我们从基本的 iris 容器开始。

首先,我们将仓库(和“秘密”目录)复制到容器中。

接下来,我们将许可证密钥和 GitLab.xml 复制到 mgr 目录中。

然后,我们将密码更改为 pwd.txt 中的值。 请注意,此操作会删除 pwd.txt。

之后,实例启动并执行 load_ci.script。

最后,iris 实例停止。

以下是作业日志(部分日志,跳过了加载/编译日志):

Running with gitlab-runner 10.6.0 (a3543a27)
  on docker 7b21e0c4
Using Shell executor...
Running on docker...
Fetching changes...
Removing ci/
Removing temp.txt
HEAD is now at 5ef9904 Build load_ci.script
From http://gitlab.eduard.win/test/docker
   5ef9904..9753a8d  master     -> origin/master
Checking out 9753a8db as master...
Skipping Git submodules setup
$ cp -r /InterSystems/mount ci
$ cd ci
$ echo 'SuperUser' | cat - pwd.txt load_ci.script > temp.txt
$ mv temp.txt load_ci.script
$ cd ..
$ docker build --build-arg CI_PROJECT_DIR=$CI_PROJECT_DIR -t docker.eduard.win/test/docker:$CI_COMMIT_REF_NAME .
Sending build context to Docker daemon  401.4kB

Step 1/6 : FROM docker.intersystems.com/intersystems/iris:2018.1.1.611.0 ---> cd2e53e7f850 Step 2/6 : ENV SRC_DIR=/tmp/src ---> Using cache ---> 68ba1cb00aff Step 3/6 : ENV CI_DIR=$SRC_DIR/ci ---> Using cache ---> 6784c34a9ee6 Step 4/6 : ENV CI_PROJECT_DIR=$SRC_DIR ---> Using cache ---> 3757fa88a28a Step 5/6 : COPY ./ $SRC_DIR ---> 5515e13741b0 Step 6/6 : RUN cp $CI_DIR/iris.key $ISC_PACKAGE_INSTALLDIR/mgr/ && cp $CI_DIR/GitLab.xml $ISC_PACKAGE_INSTALLDIR/mgr/ && $ISC_PACKAGE_INSTALLDIR/dev/Cloud/ICM/changePassword.sh $CI_DIR/pwd.txt && iris start $ISC_PACKAGE_INSTANCENAME && irissession $ISC_PACKAGE_INSTANCENAME -U%SYS < $CI_DIR/load_ci.script && iris stop $ISC_PACKAGE_INSTANCENAME quietly ---> Running in 86526183cf7c . Waited 1 seconds for InterSystems IRIS to start This copy of InterSystems IRIS has been licensed for use exclusively by: ISC Internal Container Sharding Copyright (c) 1986-2018 by InterSystems Corporation Any other use is a violation of your license agreement

%SYS> 1

%SYS> Using 'iris.cpf' configuration file

This copy of InterSystems IRIS has been licensed for use exclusively by: ISC Internal Container Sharding Copyright (c) 1986-2018 by InterSystems Corporation Any other use is a violation of your license agreement

1 alert(s) during startup. See messages.log for details. Starting IRIS

Node: 39702b122ab6, Instance: IRIS

Username: Password:

Load started on 04/06/2018 17:38:21 Loading file /usr/irissys/mgr/GitLab.xml as xml Load finished successfully.

USER>

USER>

[2018-04-06 17:38:22.017] Running init hooks: before

[2018-04-06 17:38:22.017] Importing hooks dir /tmp/src/MyApp/Hooks/

[2018-04-06 17:38:22.374] Executing hook class: MyApp.Hooks.Global

[2018-04-06 17:38:22.375] Executing hook class: MyApp.Hooks.Local

[2018-04-06 17:38:22.375] Importing dir /tmp/src/

Loading file /tmp/src/MyApp/Tests/TestSuite.cls as udl

Compilation started on 04/06/2018 17:38:22 with qualifiers 'c' Compilation finished successfully in 0.194s.

Load finished successfully.

[2018-04-06 17:38:22.876] Running init hooks: after

[2018-04-06 17:38:22.878] Executing hook class: MyApp.Hooks.Local

[2018-04-06 17:38:22.921] Executing hook class: MyApp.Hooks.Global Removing intermediate container 39702b122ab6 ---> dea6b2123165 [Warning] One or more build-args [CI_PROJECT_DIR] were not consumed Successfully built dea6b2123165 Successfully tagged docker.domain.com/test/docker:master Job succeeded

请注意,我使用的是 GitLab Shell 执行器,而不是 Docker 执行器。 当您需要从镜像内部提取某些内容时,将使用 Docker 执行器,例如在 Java 容器中构建 Android 应用程序并且只需要一个 apk 时。 在我们的示例中,我们需要整个容器,因此需要使用 Shell 执行器。 因此,我们通过 GitLab Shell 执行器运行 Docker 命令。

 

运行

我们构建了镜像,接下来要运行镜像。如果是功能分支,我们销毁旧容器并启动新容器即可。 如果是环境,我们可以运行临时容器,在测试成功的情况下,可以替换环境容器(此内容留给读者作为练习)。

脚本如下。

destroy old:
  stage: destroy
  tags:
    - test
  script:
    - docker stop iris-$CI_COMMIT_REF_NAME || true
    - docker rm -f iris-$CI_COMMIT_REF_NAME || true

此脚本会销毁当前运行的容器,并且始终都会成功(默认情况下,如果 docker 尝试停止/移除不存在的容器,则会失败)。

接下来,我们启动新镜像并将其注册为环境。 Nginx 容器 使用 VIRTUAL_HOST 环境变量和 expose 指令(用于了解要代理的端口)自动代理请求。

run image:
  stage: run
  environment:
    name: $CI_COMMIT_REF_NAME
    url: http://$CI_COMMIT_REF_SLUG. docker.domain.com/index.html
  tags:
    - test
  script:
    - docker run -d
      --expose 52773
      --env VIRTUAL_HOST=$CI_COMMIT_REF_SLUG.docker.eduard.win
      --name iris-$CI_COMMIT_REF_NAME
      docker.domain.com/test/docker:$CI_COMMIT_REF_NAME
      --log $ISC_PACKAGE_INSTALLDIR/mgr/messages.log

 

测试

我们来运行一些测试。

test image:
  stage: test
  tags:
    - test
  script:
    - docker exec iris-$CI_COMMIT_REF_NAME irissession iris -U USER "##class(isc.git.GitLab).test()"

发布

最后,我们将镜像发布到注册表中

publish image:
  stage: publish
  tags:
    - test
  script:
    - docker login docker.domain.com -u dev -p 123
    - docker push docker.domain.com/test/docker:$CI_COMMIT_REF_NAME

可以使用 GitLab 秘密变量传递用户名/密码。

现在,我们可以在 GitLab 中看到该镜像:

其他开发者可以从注册表中拉取该镜像。 “环境”标签页上提供了我们所有的环境,可以轻松进行浏览:

 

结论

在这一系列文章中,我介绍了持续交付的常规方式。 这是一个涉及面非常广的话题,您应将这一系列文章视为方法集合,而不是确定性内容。 如果您想自动构建、测试和交付应用程序,可以选择持续交付(一般情况)和 GitLab(特殊情况)。 利用持续交付和容器,您可以根据需要自定义工作流。

链接

后续内容

就是这些! 希望我已介绍了持续交付和容器的基础知识。

但还有一些主题我没有介绍(可能后面会介绍),特别是关于容器的内容:

  • 数据可以在容器外持久保持,以下是相关文档
  • kubernetes 等编排平台
  • InterSystems Cloud Manager
  • 环境管理 – 创建临时环境以进行测试,在功能分支合并后移除旧环境
  • Docker compose 可以实现多容器部署
  • 缩减 docker 镜像大小并缩短构建时间
  • ...
0
0 110
文章 Michael Lei · 九月 27, 2024 2m read

在这一系列文章中,我想向大家介绍并探讨使用 InterSystems 技术和 GitLab 进行软件开发可以采用的几种方式。 我将介绍以下主题:

  • Git 101
  • 流程(开发流程)
  • GitLab 安装
  • GitLab 工作流
  • 持续交付
  • GitLab 安装和配置
  • GitLab CI/CD
  • 为何使用容器?
  • 容器基础架构
  • 使用容器的 GitLab CI/CD

第一篇文章中,我们介绍了 Git 基础知识,以及为什么对 Git 概念的高层次理解对于现代软件开发如此重要,以及如何使用 Git 开发软件。

第二篇文章中,我们介绍了 GitLab 工作流 – 一个完整的软件生命周期流程和持续交付。

第三篇文章中,我们介绍了 GitLab 的安装和配置以及如何将环境连接到 GitLab。

第四篇文章中,我们编写了 CD 配置。

第五篇文章中,我们讨论了容器与如何(以及为什么)使用它们。

在本文中,我们将探讨运行使用容器的持续交付管道所需的主要组件,以及它们如何协同工作。

我们的配置如下所示:

在这里,我们可以看到三个主要阶段的分离:

  • 构建
  • 传送
  • 运行

构建

在之前的部分中,构建通常是增量式 – 我们计算当前环境与当前代码库之间的差异,并修改我们的环境以与代码库相对应。 使用容器时,每次构建都是完整构建。 构建的结果是一个可以通过依赖关系在任何地方运行的镜像。

传送

在我们的镜像构建并通过测试后,它会被上传到注册表 – 用于托管 docker 镜像的专用服务器。 在那里,它可以替换具有相同标签的旧镜像。 例如,由于对 master 分支的新提交,我们构建了新镜像 (project/version:master),如果测试通过,我们可以用相同标签的新镜像替换注册表中的旧镜像,这样,所有拉取 project/version:master 的人都会获得新版本。

运行

最后,我们的镜像部署完成。 CI 解决方案(如 GitLab)可以控制此流程,也可以由专门的编排器控制,但目的都一样 – 执行一些镜像,定期检查健康状态,如果有新镜像版本可用就进行更新。

查看 docker 网络研讨会以了解这些不同阶段的解释。

或者,从提交的角度来看:<

在我们的交付配置中,我们会:

  • 将代码推送到 GitLab 仓库
  • 构建 docker 镜像
  • 测试该镜像
  • 将镜像发布到我们的 docker 注册表
  • 用新版本替换注册表中的旧容器

为此,我们需要:

  • Docker
  • Docker 注册表
  • 注册域(可选但推荐)
  • GUI 工具(可选)

 

Docker

首先,我们需要在某个地方运行 docker。 我建议从一台运行主流 Linux 发行版(如 Ubuntu、RHEL 或 Suse)的服务器开始。 不要使用面向云的发行版(如 CoreOS、RancherOS 等) - 它们并不适合初学者。 别忘了将存储驱动程序切换为 devicemapper。 

如果我们在讨论大规模部署,那么使用像 Kubernetes、Rancher 或 Swarm 这样的容器编排工具可以自动执行大多数任务,但我们不会探讨它们(至少在这一部分中)。

 

Docker 注册表

这是我们需要运行的第一个容器,它是一个无状态、可扩缩的服务器端应用程序,作用是存储和分发 Docker 镜像。
您应当在想要实现以下目的时使用注册表:

  •  严格控制您的镜像存储位置
  •  完全掌控您的镜像分发管道
  •  将镜像存储和分发紧密整合到您的内部开发工作流中

此处提供了注册表的文档

连接注册表和 GitLab

注:GitLab 包含内置注册表。 您可以运行它而不使用外部注册表。 请阅读本段中链接的 GitLab 文档。

要将您的注册表连接到 GitLab,您需要运行支持 HTTPS的注册表 – 我使用 Let's Encrypt 获取证书,并按照此 Gist 获取证书并将其传递到容器中。 在确认注册表可以通过 HTTPS 访问后(您可以从浏览器检查),请按照这些说明将注册表连接到 GitLab。根据您的需要和 GitLab 安装情况,这些说明会有所不同。就我而言,配置是将注册表证书和密钥(正确命名且具有正确的权限)添加到 /etc/gitlab/ssl 并将以下行添加到 /etc/gitlab/gitlab.rb 中:

registry_external_url 'https://docker.domain.com'
gitlab_rails['registry_api_url'] = "https://docker.domain.com"

重新配置 GitLab 后,我可以看到一个新的注册表标签页,其中提供了如何正确为新构建的镜像添加标签以便它们会出现在这里的信息。

 

在我们的持续交付配置中,我们将为每个分支自动构建一个镜像,如果镜像通过测试,那么我们会将其发布到注册表并自动运行,因此我们的应用程序将在所有“状态”下自动可用,例如,我们可以访问:

  • 几个功能分支,网址为 <featureName>.docker.domain.com
  • 测试版本,网址为 master.docker.domain.com
  • 试生产版本,网址为 preprod.docker.domain.com
  • 生产版本,网址为 prod.docker.domain.com

为此,我们需要一个域名并添加一条通配符 DNS 记录,将 *.docker.domain.com 指向 docker.domain.com 的 IP 地址。 另一种选项是使用不同的端口。

Nginx 代理

由于我们有几个功能分支,需要将子域自动重定向到正确的容器。 为此,我们可以使用 Nginx 作为反向代理。 此处提供了一份指南

GUI 工具

要开始使用容器,您可以使用命令行或 GUI 界面之一。 有许多可用工具,例如:

  • Rancher
  • MicroBadger
  • Portainer
  • Simple Docker UI
  • ...

它们允许您从 GUI 而不是 CLI 创建和管理容器。 下面是 Rancher 的外观:

 

GitLab Runner

与之前相同,要在其他服务器上执行脚本,我们需要安装 GitLab 运行程序。 我在第三篇文章中探讨过这一点。

请注意,您需要使用 Shell 执行器而不是 Docker 执行器。 当您需要从镜像内部提取某些内容时,将使用 Docker 执行器,例如在 Java 容器中构建 Android 应用程序并且只需要一个 apk 时。 在我们的示例中,我们需要整个容器,因此需要使用 Shell 执行器。

 

结论

开始运行容器十分容易,并且有许多工具可供选择。

使用容器的持续交付与常规的持续交付配置在以下几个方面有所不同:

  • 依赖关系的需求在构建时得到满足,并且在镜像构建后无需考虑依赖关系。
  • 可重现性 – 您可以通过本地运行相同的容器轻松重现任何现有环境。
  • 速度 – 由于容器仅包含您显式添加的内容,它们的构建速度更快,更重要的是,它们仅需构建一次并在需要时使用。
  • 效率 – 同上,与虚拟机等相比,容器产生的开销更少。
  • 可扩缩性 – 使用编排工具,您可以根据工作负载自动扩缩应用程序,并且仅消耗当前所需的资源。

后续内容

在下一篇文章中,我们将创建一个利用 InterSystems IRIS Docker 容器的 CD 配置。

0
0 148
文章 Jingwei Wang · 三月 28, 2023 5m read

IRIS 配置和用户帐户包含需要跟踪的各种数据元素,许多人难以在 IRIS 实例之间复制或同步这些系统配置和用户帐户。那么如何简化这个过程呢?

在软件工程中,CI/CD 或 CICD 是持续集成 (CI) 和(更常见的)持续交付或(较少见的)持续部署 (CD) 的组合实践集。 CI/CD 能消除我们所有的挣扎吗?

我在一个开发和部署 IRIS 集群的团队工作。我们在 Red Hat OpenShift 容器平台上的容器中运行 IRIS。

如果您当前没有使用 Kubernetes,请不要停止阅读。即使您没有使用 Kubernetes 或在容器中运行 IRIS,您也可能会遇到与我和我的团队面临的挑战类似的挑战。

我们决定将代码与配置分开,并将它们放在不同的 GitHub 存储库中。每次在代码库中进行提交时,都会触发管道运行。结果,从代码库中的文件构建了一个新image。

我们通过将 YAML 文件和其他配置工件添加到部署 GitHub 存储库,将配置定义为以 GitOps 方式使用的代码。 GitOps 是一个软件开发框架,它使组织能够持续交付软件应用程序,同时使用 Git 作为单一事实来源有效地管理 IT 基础设施(以及更多)。 GitOps 的好处之一是能够轻松回滚。您所需要做的就是恢复到 Git 中的先前状态。

0
0 315
文章 Michael Lei · 十二月 9, 2022 7m read

在数量众多、形形色色的 SQL 数据库市场中,InterSystems IRIS 作为一个超越 SQL 的平台脱颖而出,它提供无缝的多模型体验,支持丰富的开发范式。 特别是,先进的对象-关系引擎已经帮助组织为其数据密集型工作负载的每个方面使用了最适合的开发方式,例如在通过对象获取数据并同时通过 SQL 查询数据。 持久类与 SQL 表相对应,其属性与表中的各列相对应,可以使用用户定义的函数或存储过程轻松访问业务逻辑。 在这篇文章中,我们将深入了解表面之下的一点底层技术,讨论它可能如何影响您的开发和部署方式。 这是我们计划发展和改进的产品领域,因此请不要犹豫,在下面的评论区分享您的观点和体验。

保存存储定义 {Saving the Storage Definition}

编写全新的业务逻辑很容易,而且假如您有定义明确的 API 和规范,那么调整或扩展通常也很容易。 但是,当它不仅仅是业务逻辑,还涉及持久化数据时,从初始版本更改的任何内容都将需要能够妥善处理通过早期版本获取的数据。

0
0 187
文章 Michael Lei · 十月 10, 2022 2m read

Kong提供了一个开源的配置管理工具(用Go语言编写),称为decK(即声明式Kong,declarative Kong)。

  • 通过deck ping检查deck是否能识别你的Kong Gateway安装
deck ping   
Successfully connected to Kong!
Kong version:  2.3.3.2-enterprise-edition
  • 通过deck dump把 Kong Gateway 配置导出到一个叫 "kong.yaml" 的文件
deck dump
  • 修改kong.yaml文件后, 通过 deck diff 显示区别
0
0 106
文章 Michael Lei · 四月 25, 2022 2m read

Kong提供了一个开源的配置管理工具(用Go语言编写),称为decK(代表声明式Kong)

  • 通过deck ping检查deck是否能识别你的Kong Gateway安装
deck ping   
Successfully connected to Kong!
Kong version:  2.3.3.2-enterprise-edition
  • 通过deck dump把 Kong Gateway配置倒出到 "kong.yaml" 文件 
deck dump
  • 修改 kong.yaml以后通过deck diff 通过把区别显示出来
0
0 383
文章 Hao Ma · 三月 26, 2021 15m read

关键字:IRIS,IntegratedML,Flask,FastAPI,TensorFlow Serving,HAProxy,Docker,Covid-19

目的:

过去几个月里,我们提到了一些深度学习和机器学习的快速演示,包括一个简单的 Covid-19 X 射线图像分类器和一个用于可能的 ICU 入院的 Covid-19 实验室结果分类器。  我们还介绍了 ICU 分类器的 IntegratedML 演示实现。  虽然“数据科学”远足仍在继续,但从“数据工程”的角度来看,或许也是尝试一些 AI 服务部署的好时机 - 我们能否将目前所接触到的一切都封装成一套服务 API?  我们可以利用哪些常用的工具、组件和基础架构,以最简单的方式实现这样的服务堆栈?

0
0 1420
文章 Jeff Liu · 一月 7, 2021 5m read

非常高兴地宣布,InterSystems 容器注册表现在可以使用了。 这为客户访问基于容器的版本及预览提供了新的渠道。 所有的社区版像都可在公共存储库中找到,且无需登录。 所有完整发布的像(IRIS、IRIS for Health、Health Connect、System Alerting and Monitoring、InterSystems Cloud Manager)和实用程序镜像(例如,仲裁器、Web 网关和 PasswordHash)都需要登录令牌,该令牌从 WRC 帐户生成。WRC 发布网站暂时将继续以 tarball 方式提供已发布像。 不过,您现在可以配置 CI/CD 管道以直接从 InterSystems 容器注册表“docker pull”镜像。
可通过 https://containers.intersystems.com 访问该注册表。 有关完整的使用说明,请参阅下文或参阅文档(使用 InterSystems 容器注册表)。如果您遇到任何问题或有任何反馈要分享,请在下面的评论中告知我们,或联系 support@intersystems.com。
--------------------------------------------------------------

使用 InterSystems 容器注册表

本文档列出了 InterSystems 容器注册表 (ICR) 中可用的像,并提供了使用说明。该注册表位于 containers.intersystems.com 上。

可以使用 docker pull 命令下载 ICR 中的像,例如:

docker pull containers.intersystems.com/intersystems/iris-community:2020.3.0.221.0

本文档包含以下部分:

  • 公共像
  • 受限访问镜像
  • 对 ICR 进行身份验证
  • 列出 ICR 清单

公共镜像

以下 ICR 像是公开可用的,无需身份验证即可拉取:

InterSystems IRIS

IntegratedML

2020.3

containers.intersystems.com/intersystems/iris-ml-community:2020.3.0.302.0

Community Edition

2020.3

containers.intersystems.com/intersystems/iris-community:2020.3.0.221.0

2020.3 ARM64

containers.intersystems.com/intersystems/iris-community-arm64:2020.3.0.221.0

InterSystems IRIS for Health

IntegratedML2020.3containers.intersystems.com/intersystems/irishealth-ml-community:2020.3.0.302.0
Community Edition

2020.3

containers.intersystems.com/intersystems/irishealth-community:2020.3.0.221.0

2020.3 ARM64

containers.intersystems.com/intersystems/irishealth-community-arm64:2020.3.0.221.0

System Alerting and Monitoring

 

1.0

containers.intersystems.com/intersystems/sam:1.0.0.115

以下 ICR 镜像仅对经过身份验证的用户可用:受限访问镜像

以下 ICR 像是公开可用的,无需身份验证即可拉取:

Arbiter

2020.1

containers.intersystems.com/intersystems/arbiter:2020.1.0.215.0

2020.2

containers.intersystems.com/intersystems/arbiter:2020.2.0.211.0

2020.3

containers.intersystems.com/intersystems/arbiter:2020.3.0.210.0

Health Connect

2020.1

containers.intersystems.com/intersystems/healthconnect:2020.1.0.215.0

InterSystems Cloud Manager (ICM)

2020.1

containers.intersystems.com/intersystems/icm:2020.1.0.215.0

2020.2

containers.intersystems.com/intersystems/icm:2020.2.0.211.0

2020.3

containers.intersystems.com/intersystems/icm:2020.3.0.221

InterSystems IRIS

2020.1

containers.intersystems.com/intersystems/iris:2020.1.0.215.0

2020.2

containers.intersystems.com/intersystems/iris:2020.2.0.211.0

2020.3

containers.intersystems.com/intersystems/iris:2020.3.0.221.0

2020.1 ARM64

containers.intersystems.com/intersystems/iris-arm64:2020.1.0.215.0

2020.2 ARM64

containers.intersystems.com/intersystems/iris-arm64:2020.2.0.211.0

2020.3 ARM64

containers.intersystems.com/intersystems/iris-arm64:2020.3.0.221.0

2020.3 IntegratedMLcontainers.intersystems.com/intersystems/iris-ml:2020.3.0.302.0

InterSystems IRIS for Health

2020.1

containers.intersystems.com/intersystems/irishealth:2020.1.0.217.1

2020.2

containers.intersystems.com/intersystems/irishealth:2020.2.0.211.0

2020.3

containers.intersystems.com/intersystems/irishealth:2020.3.0.221.0

2020.1 ARM64

containers.intersystems.com/intersystems/irishealth-arm64:2020.1.0.217.1

2020.2 ARM64

containers.intersystems.com/intersystems/irishealth-arm64:2020.2.0.211.0

2020.3 ARM64

containers.intersystems.com/intersystems/irishealth-arm64:2020.3.0.221.0

2020.3 IntegratedMLcontainers.intersystems.com/intersystems/irishealth-ml:2020.3.0.302.0

PasswordHash

1.0

containers.intersystems.com/intersystems/passwordhash:1.0

Web Gateway

2020.2

containers.intersystems.com/intersystems/webgateway:2020.2.0.211.0

2020.3

containers.intersystems.com/intersystems/webgateway:2020.3.0.221.0


要登录至 ICR,请执行以下步骤:对 ICR 进行身份验证

  • 在您的浏览器中加载 https://containers.intersystems.com/,然后使用您的 InterSystems/WRC 凭据登录。
  • 检索您的 Docker 登录令牌或完整的登录命令。
  • 在 Docker 界面(例如,PowerShell 窗口或 Linux 命令行)中,使用提供的凭据对 ICR 进行身份验证。 您可以通过复制并粘贴显示的完整 docker login 命令来执行此操作,例如:
docker login -u="bbinstoc" -p="provided_password" containers.intersystems.com

但是,出于安全原因,您可能想要输入命令 docker login container.intersystems.com,然后在 Username 提示符下输入用户名并将密码粘贴到 Password: 提示符下。

注意:如果您登录到另一个 Docker 注册表,则 docker login 命令可能会导致错误;登录到 container.intersystems.com 之前,请先注销其他注册表。

  1. 现在,您可以从 ICR 中拉取镜像,例如:
docker pull containers.intersystems.com/intersystems/iris:2020.3.0.221.0

列出 ICR 清单

API 可用于列出 Docker 注册表中的镜像和标签。 可用于列出注册表清单的开源第三方实用程序的一个示例是 docker-ls ,其可从 https://github.com/mayflower/docker-ls 获取。

获取此实用程序的方法有几种。 你可以:

  • 下载用于各种平台的预编译 docker-ls 二进制文件
  • 直接在某些平台上安装该实用程序,例如,在 Linux 系统上使用以下命令进行安装:
sudo snap install docker-ls
  • 在 Linux 平台上拉取并运行镜carinadigital/docker-ls:latest 以安装该实用程序,例如:
docker run --rm carinadigital/docker-ls:latest

安装 docker-ls 后,您可以使用以下命令列出 ICR 中的存储库:

docker-ls repositories --registry https://containers.intersystems.com --user username --password password

注意:使用 --interactive-password 选项提示输入密码,不要在命令行中输入密码。

要仅列出公开可用的像,请为 -user --password 选项提供空字符串 ("") 作为参数, 例如,以下仅列出了公共 InterSystems IRIS for Health 镜像的标签:

docker-ls tags --registry https://containers.intersystems.com --user "" --password "" intersystems/irishealth-community

如果希望看到非公共像的完整列表,则无论是否登录 container.intersystems.com,都需要向该实用程序提供用户名和密码。

可访问 https://github.com/mayflower/docker-ls 了解其他示例。

0
0 178