【k8s深入理解之 Scheme 补充-4】理解资源的内外部版本(存放位置不同、版本转换函数自动生成、版本新增字段等)

附录1 | types 的存放位置(为什么内部版本和外部版本在不同位置)

为什么 apps 组的 internal 的 types.go 放在 k8s.io/kubernetes 项目中,而 v1 等版本的 types.go 放在 ks8.io/api 项目中

Kubernetes 将 API 类型划分为 internal 和外部版本(如 v1, v1beta1 等),并采用不同的目录存放是为了支持 代码清晰性版本管理 的需求。这种组织方式遵循了 Kubernetes 的 API 设计哲学,目的是在保证向后兼容的同时实现灵活的内部实现。以下是原因的详细分析:


1. Internal API 的定位

  • 定义: Internal 类型是 Kubernetes 内部使用的原始 API 类型,它们代表的是核心逻辑中的资源对象,通常不直接暴露给用户或外部客户端。
  • 放在 k8s.io/kubernetes 目录的原因:
    • 直接服务于 Kubernetes 核心组件: Internal 类型仅供 Kubernetes 核心组件(如 Controller、API Server)使用,因此与 Kubernetes 的核心实现绑定。放在 k8s.io/kubernetes 下,可以方便地进行内部访问。
    • 更高的修改自由度: Internal 类型允许 Kubernetes 开发者在不考虑向后兼容的情况下优化或调整数据结构。用户和外部客户端永远不会直接与 Internal 类型交互,因此可以灵活调整其设计。
  • 特点:
    • 不暴露给外部用户。
    • 更接近 Kubernetes 的核心逻辑。
    • 需要与外部版本进行显式转换(通过 conversion 函数)。

2. 外部版本 API 的定位

  • 定义: 外部版本(如 v1, v1beta1)是公开的 API 类型,用户通过 Kubernetes API(如 kubectl 或 client-go)访问这些资源。
  • 放在 k8s.io/api 目录的原因:
    • 模块化设计: Kubernetes 将公开 API 类型放在 k8s.io/api 模块中,使其与核心实现解耦,从而便于外部开发者独立使用,而无需依赖整个 k8s.io/kubernetes 项目。
    • API 的稳定性需求: 外部版本需要严格遵循 Kubernetes 的 API 版本管理规则(如语义版本控制),放在一个独立的模块中可以清晰地区分不同版本的 API,并方便管理稳定性。
    • 客户端使用的便利性: 通过 k8s.io/api,客户端(如 kubectl 或自定义控制器)可以直接引用所需的资源定义,而不需要引入庞大的 Kubernetes 源码。
  • 特点:
    • 面向用户和外部客户端。
    • 保证向后兼容性(不会轻易修改字段或结构)。
    • 对应具体的 API 版本(如 apps/v1)。

3. 为什么分开存储

Kubernetes 的 API 类型和实现是严格分离的,这种设计有以下好处:

特性 Internal 类型 外部版本
用途 Kubernetes 内部使用,支撑核心逻辑 面向用户和客户端,用于操作资源
修改自由度 可自由修改,不需要向后兼容 必须遵循向后兼容性规则
版本管理 无版本概念,保持内部逻辑一致即可 需要严格区分版本,遵循语义版本(v1, v1beta1)
依赖结构 位于 k8s.io/kubernetes,与核心代码强绑定 位于 k8s.io/api,与核心实现解耦

4. 转换逻辑

Kubernetes 通过 conversion 函数 实现 Internal 类型和外部版本之间的转换。

  • 转换逻辑通常位于 k8s.io/kubernetes 中,例如:

    • pkg/apis/apps/v1/conversion.go:用于 apps/v1 和 internal 类型之间的转换。
  • API Server 接收到外部请求时,会将外部版本(如 apps/v1)的对象转换为 Internal 类型进行处理。处理完成后,结果再转换回外部版本发送给用户。


5. 总结

  • Internal 类型存放在 k8s.io/kubernetes 是因为它们仅供 Kubernetes 内部组件使用,与核心实现逻辑紧密相关。
  • 外部版本类型存放在 k8s.io/api 是为了模块化设计、方便客户端使用,并明确区分对外 API 和内部实现。
  • 这种设计确保了 Kubernetes 的灵活性、稳定性和可维护性,同时方便了外部开发者与 Kubernetes API 的交互。

附录2 | 外部版本的转换需要内部版本中转

  • 假如没有内部版本,一个 Group 中有 5 个版本,那么需要建立【两两互相转换关系】,也就是 C(5,2),需要10种排列组合
  • 但是若有内部版本,这5个版本仅需要和内部版本建立互相转换关系,只需要 5 种排列组合,大大减少所需要的转换函数

在 Kubernetes 中,外部版本与内部版本之间的转换是通过 显式定义的转换逻辑 实现的。这种设计确保了不同版本的 API 能够保持兼容性,同时使内部版本的结构更适合核心逻辑处理。以下是转换机制的详细解析,包括内部版本是否包含所有外部字段以及转换过程是否需要通过内部版本中转。


1. 外部版本和内部版本的转换实现

转换机制

Kubernetes 提供了一套转换机制,用于在外部版本(如 apps/v1)和内部版本(如 apps)之间转换:

  • 内部版本

    • Kubernetes 的内部版本(Internal Version)是核心逻辑中使用的数据结构,不依赖 API 的具体版本。
    • 内部版本是所有外部版本的中间表示,通常简化了字段并为核心逻辑优化。
  • 外部版本

    • 外部版本(如 apps/v1, apps/v1beta1)是面向用户和客户端的版本化 API。
    • 每个外部版本与内部版本存在双向映射(从外部到内部,或从内部到外部)。
转换逻辑

转换逻辑通过 Kubernetes 的 conversion-gen 工具自动生成,或在需要自定义逻辑时手动实现。

  • 生成代码

    • 使用 conversion-gen 生成默认的字段映射逻辑。
    • 自动生成的代码根据字段名称和类型的匹配关系进行转换。
  • 自定义逻辑

    • 当外部版本的字段与内部版本的字段结构不一致时,可以通过 manualConvert 方法手动实现特殊转换逻辑。
核心工具
  • Schemeruntime.Scheme 负责管理类型的注册、版本信息以及转换函数。
  • converter :通过 Scheme.converter 实现具体的版本间转换。

示例代码:

go 复制代码
scheme.AddConversionFuncs(
    func(in *v1.Pod, out *core.Pod, s conversion.Scope) error {
        out.Spec = in.Spec
        out.ObjectMeta = in.ObjectMeta
        return nil
    },
    func(in *core.Pod, out *v1.Pod, s conversion.Scope) error {
        out.Spec = in.Spec
        out.ObjectMeta = in.ObjectMeta
        return nil
    },
)

2. 内部版本是否包含所有外部版本的字段?

答案:不完全包含,但能表达外部版本的语义。

内部版本的设计是为了简化和优化核心逻辑,因此它可能不会直接包含所有外部版本的字段:

  • 包含主要字段

    • 内部版本通常会包含外部版本的大部分核心字段,确保能处理这些字段的逻辑。
  • 省略过时字段

    • 如果某些字段仅在旧版本中存在且已弃用,内部版本可能不会直接包含这些字段。
  • 统一字段表示

    • 不同外部版本中的某些字段可能在内部版本中被统一表示。例如,多个外部字段可以映射到一个内部字段。
  • 注解或扩展字段

    • 如果某些外部字段不适合直接添加到内部版本,可能会通过注解或扩展字段的方式表示。

示例

  • apps/v1.Deployment 在外部版本中可能有字段 strategy.rollingUpdate.maxUnavailable,但在内部版本中可能表示为一个通用的 RollingUpdateStrategy 结构。

3. 外部版本的转换是否需要通过内部版本中转?

答案:是,通常需要通过内部版本中转。

Kubernetes 的转换机制采用 中心化设计 ,所有外部版本之间的转换必须通过内部版本。

例如:

  • 流程

    1. apps/v1.Deployment 转换为内部版本 apps.Deployment
    2. 将内部版本 apps.Deployment 转换为 apps/v1beta1.Deployment
  • 原因

    • 统一的核心逻辑

      • 内部版本是核心逻辑的唯一表示。所有控制器和调度器等组件只处理内部版本,外部版本只在 API 层使用。
    • 减少复杂性

      • 如果每个外部版本直接互相转换,会导致 n*(n-1) 个转换函数(n 为外部版本的数量)。通过内部版本中转,只需要 2n 个转换函数(每个外部版本到内部版本的双向转换)。
    • 支持演进

      • 新的外部版本可以轻松地添加转换逻辑,而不影响现有逻辑。

4. 转换的具体流程

假设需要将 apps/v1.Deployment 转换为 apps/v1beta1.Deployment

  1. 从外部到内部

    • API Server 接收到 apps/v1.Deployment 对象。
    • 使用转换函数将其转换为内部版本 apps.Deployment
  2. 从内部到目标外部版本

    • 使用转换函数将内部版本 apps.Deployment 转换为 apps/v1beta1.Deployment
  3. 返回结果

    • 将最终结果返回给用户或客户端。

5. 转换过程中的注意点

  • 字段的默认值处理

    • 转换过程中,某些字段可能需要设置默认值(默认值由 API Server 提供)。
    • Kubernetes 使用 defaulter-gen 自动生成字段的默认值设置代码。
  • 字段的丢弃与填充

    • 在从新版本转换到旧版本时,可能会丢弃在旧版本中不存在的字段。
    • 在从旧版本转换到新版本时,新增字段可能需要填充默认值。
  • 自定义字段映射

    • 如果字段名称或类型不直接匹配,需要自定义转换函数。

总结

  1. 如何实现转换
    • 外部版本和内部版本通过 conversion-gen 自动生成的转换函数或手动定义的逻辑实现转换。
    • 转换逻辑由 runtime.Schemeconverter 管理。
  2. 内部版本是否包含所有外部字段
    • 内部版本不完全包含所有外部字段,但足以表达外部版本的语义。
    • 某些字段可能通过注解或扩展字段间接表示。
  3. 外部版本转换是否通过内部版本中转
    • 是的,所有外部版本的转换都需要经过内部版本中转。这种中心化设计简化了逻辑和维护。
  4. 转换机制的意义
    • 保持 API 的向后兼容性。
    • 统一核心逻辑,减少重复实现。

附录3 | 转换函数由 conversion-gen 自动生成

以 Kubernetes 的 Deployment 为例,conversion-gen 工具会生成一系列函数用于在不同版本的 Deployment 对象之间进行双向转换。以下是这些函数的作用及其如何处理新增字段的详细解析。


1. conversion-gen 生成的函数

conversion-gen 生成的函数包括以下几类:

(1) Convert_\* 函数
  • 形式Convert_SOURCE_TO_DEST,如:

    go 复制代码
    func Convert_v1_Deployment_to_apps_Deployment(in *v1.Deployment, out *apps.Deployment, s conversion.Scope) error
  • 功能

    • v1.Deployment 转换为内部版本的 apps.Deployment,或反向转换。
    • 每个字段的转换逻辑都在这些函数中实现。
  • 自动生成逻辑

    • 如果 inout的字段名称、类型一致,conversion-gen会直接生成赋值逻辑:out.FieldName = in.FieldName

    • 如果字段类型不同,需要手动定义转换逻辑并注册到 Scheme 中。

(2) autoConvert_\* 函数
  • 形式autoConvert_SOURCE_TO_DEST,如:

    go 复制代码
    func autoConvert_v1_Deployment_to_apps_Deployment(in *v1.Deployment, out *apps.Deployment, s conversion.Scope) error
  • 功能

    • Convert_* 类似,但是默认生成的基础转换逻辑。
    • 如果没有复杂的自定义逻辑,这些函数会直接调用。
(3) 自定义转换逻辑
  • 如果字段的转换需要特定的自定义逻辑(如类型不匹配,或需根据上下文计算),开发者需要手动定义函数,并通过 Scheme 注册。

  • 示例:

    go 复制代码
    func Convert_v1_DeploymentSpec_to_apps_DeploymentSpec(in *v1.DeploymentSpec, out *apps.DeploymentSpec, s conversion.Scope) error {
        // 自定义字段处理
        if in.Replicas != nil {
            out.Replicas = new(int32)
            *out.Replicas = *in.Replicas
        }
        return nil
    }
(4) AddConversionFuncs 注册函数
  • conversion-gen会生成注册函数,用于将所有转换函数注册到 Scheme

    go 复制代码
    func RegisterConversions(scheme *runtime.Scheme) error {
        return scheme.AddConversionFuncs(
            Convert_v1_Deployment_to_apps_Deployment,
            Convert_apps_Deployment_to_v1_Deployment,
            // 更多函数注册
        )
    }

2. 新增字段的转换逻辑

Deployment 的外部版本新增字段时,转换机制的处理依赖于以下情况:

(1) 新增字段在内部版本中存在
  • 场景:外部版本新增字段,但该字段已经在内部版本中定义。

  • 处理方式

    • conversion-gen 会自动生成字段的映射逻辑。
    • 如果字段名称和类型一致,则直接赋值;否则,需要手动实现。
  • 示例: 新增字段 priority

    yaml 复制代码
    apps/v1.Deployment:
      priority: "high"
    apps/Deployment (internal):
      Priority: "high"

    自动生成的代码:

    go 复制代码
    out.Priority = in.Priority
(2) 新增字段在内部版本中不存在
  • 场景:外部版本新增字段,但内部版本没有定义相应字段。

  • 处理方式

    • 从新版本向内部版本转换时,该字段将被丢弃。
    • 从内部版本向新版本转换时,该字段需要填充默认值,或通过注解保留。
  • 示例: 新增字段 someFeatureEnabled

    yaml 复制代码
    apps/v1.Deployment:
      someFeatureEnabled: true
    apps/Deployment (internal):
      // 没有对应字段

    自动生成的代码可能会忽略该字段,或需手动实现:

    go 复制代码
    func Convert_v1_Deployment_to_apps_Deployment(in *v1.Deployment, out *apps.Deployment, s conversion.Scope) error {
        // 需要手动处理
        if in.SomeFeatureEnabled {
            out.Annotations["someFeatureEnabled"] = "true"
        }
        return nil
    }
(3) 新增字段需要复杂的计算
  • 场景:新增字段需要根据其他字段计算值,或跨字段合成值。

  • 处理方式

    • 必须手动定义转换函数。
  • 示例: 新增字段 replicaCount,计算逻辑为 replicaCount = replicas + reservedReplicas

    go 复制代码
    func Convert_v1_Deployment_to_apps_Deployment(in *v1.Deployment, out *apps.Deployment, s conversion.Scope) error {
        out.ReplicaCount = in.Replicas + in.ReservedReplicas
        return nil
    }

3. 示例:完整转换过程

假设以下新增字段:

  • 外部版本新增字段 maxSurgesomeFeatureEnabled
(1) 外部版本定义
go 复制代码
type Deployment struct {
    MaxSurge           *int32
    SomeFeatureEnabled bool
}
(2) 内部版本定义
go 复制代码
type Deployment struct {
    MaxSurge *int32
    // 不支持 `SomeFeatureEnabled`
}
(3) 转换函数
  • 从外部版本到内部版本:

    go 复制代码
    func Convert_v1_Deployment_to_apps_Deployment(in *v1.Deployment, out *apps.Deployment, s conversion.Scope) error {
        out.MaxSurge = in.MaxSurge
        // 忽略 SomeFeatureEnabled,或保留为注解
        if in.SomeFeatureEnabled {
            if out.Annotations == nil {
                out.Annotations = make(map[string]string)
            }
            out.Annotations["someFeatureEnabled"] = "true"
        }
        return nil
    }
  • 从内部版本到外部版本:

    go 复制代码
    func Convert_apps_Deployment_to_v1_Deployment(in *apps.Deployment, out *v1.Deployment, s conversion.Scope) error {
        out.MaxSurge = in.MaxSurge
        // 从注解恢复 SomeFeatureEnabled
        if val, ok := in.Annotations["someFeatureEnabled"]; ok && val == "true" {
            out.SomeFeatureEnabled = true
        }
        return nil
    }

4. 总结

  • conversion-gen 自动生成逻辑
    • 能自动处理字段名称和类型一致的情况。
    • 为复杂的字段映射保留手动实现的空间。
  • 新增字段的转换
    • 字段已存在:自动处理。
    • 字段不存在:需要丢弃、注解保留,或填充默认值。
    • 复杂逻辑:必须手动定义转换逻辑。
  • 通过中转的设计
    • 所有外部版本转换都必须通过内部版本中转,新增字段也遵循这个过程。
    • 内部版本简化核心逻辑,外部版本适配用户需求。

附录4 | internal 内部版本新增字段,其他外部版本是否也要新增对应的字段

在 Kubernetes 中,是否需要在外部版本中新增字段,取决于以下几个因素:

  1. 新增字段的设计目的:
    • 如果新增字段是为了支持新的功能或需求,且这些功能需要对外暴露给用户,那么外部版本(如 v1, v2beta1)通常需要同步新增该字段。
    • 如果新增字段仅供内部逻辑使用,外部版本可以不包含该字段。
  2. API 版本的演进策略:
    • Kubernetes 遵循API 兼容性保证。对于已发布的稳定版本(如 v1),新增字段通常需要保持向后兼容,并且应具有默认值以避免影响已有的客户端。
    • 如果是预览版本(如 alphabeta),字段的新增或调整相对灵活,但需要谨慎评估可能的影响。
  3. 代码生成的机制:
    • Kubernetes 的内部版本(internal)是未导出的,主要用于 API Server 内部逻辑。所有外部版本(external)通过代码生成工具(如 conversion-gendeepcopy-gen)与内部版本进行转换。如果外部版本不新增字段,可能导致字段在从内部版本转换到外部版本时丢失。

两种情况下的处理:

1. 内部字段需要暴露给外部版本

如果内部新增的字段需要暴露给外部版本,应在外部版本中新增对应的字段,并通过以下机制进行转换:

  • 转换逻辑:conversion.go 文件中,定义内部版本与外部版本之间的字段转换逻辑。例如,将内部新增字段的值正确地映射到外部版本的字段中。
  • 字段标注: 在字段定义中添加注解,生成所需的代码(如 deepcopyconversion)。

示例:

go 复制代码
// Internal Version
type PodInternal struct {
    Metadata  ObjectMeta
    Spec      PodSpec
    NewField  string // 新增字段
}

// External Version (v1)
type Pod struct {
    Metadata ObjectMeta
    Spec     PodSpec
    NewField string // 同步新增字段
}

conversion.go 中定义逻辑:

go 复制代码
func Convert_internal_Pod_to_v1_Pod(in *PodInternal, out *Pod, s conversion.Scope) error {
    out.Metadata = in.Metadata
    out.Spec = in.Spec
    out.NewField = in.NewField // 映射新增字段
    return nil
}

2. 内部字段不需要暴露给外部版本

如果新增字段仅用于内部逻辑处理,而外部版本不需要关心该字段,可以采取以下策略:

  • 不在外部版本中新增字段。
  • conversion.go 中定义字段的忽略逻辑。

示例:

go 复制代码
// Internal Version
type PodInternal struct {
    Metadata  ObjectMeta
    Spec      PodSpec
    InternalField string // 仅内部使用
}

// External Version (v1)
type Pod struct {
    Metadata ObjectMeta
    Spec     PodSpec
    // 无需新增 InternalField
}

conversion.go 中定义忽略逻辑:

go 复制代码
func Convert_internal_Pod_to_v1_Pod(in *PodInternal, out *Pod, s conversion.Scope) error {
    out.Metadata = in.Metadata
    out.Spec = in.Spec
    // 忽略 InternalField
    return nil
}

字段新增的注意事项

  1. 字段的默认值: 新增字段应具有默认值(通过 defaults.go 或标注生成),以确保未设置时不会影响已有逻辑。
  2. 向后兼容性: 外部版本新增字段时,必须评估对旧版客户端的影响。如果该字段对于功能的正确性不是关键,通常应设置为可选字段。
  3. API 审核: Kubernetes 对 API 的修改通常需要经过严格的审查,尤其是对于稳定版本的修改。
  4. 版本演进: 如果新增字段对现有版本的 API 结构影响较大,可能需要通过发布新版本(如 v2)进行支持,而非直接修改现有版本。

总结

  • 新增字段需要暴露时: 外部版本需要同步新增字段,并通过转换逻辑正确映射。
  • 新增字段不需要暴露时: 外部版本可以不新增字段,但需要在转换逻辑中妥善处理,避免不必要的错误或字段丢失。

附录5 | 外部版本新增的重要字段,内部版本也要增加

不一定。在 Kubernetes 的设计中,外部版本新增字段时,是否需要在内部版本中新增对应字段取决于该字段的用途和核心逻辑的需求。以下是更具体的说明:


1. 内部版本必须新增对应字段的情况

如果外部版本的新增字段是核心逻辑所需的,则需要在内部版本中新增对应字段。这是因为:

  • 内部版本是核心逻辑的基础:所有控制器(如 Deployment Controller)只操作内部版本。如果字段对核心逻辑有影响,内部版本必须能表示该字段。
  • 数据完整性:新增字段可能需要在转换过程中从外部版本传递到内部版本,然后进一步传递到其他外部版本。
示例:maxSurge
  • 假设外部版本新增字段 maxSurge,用于定义滚动更新时的最大可用副本数。
  • 如果滚动更新逻辑依赖该字段,则需要在内部版本中新增 MaxSurge

代码示例:

go 复制代码
// apps/v1.Deployment (外部版本)
type Deployment struct {
    MaxSurge *int32
}

// apps.Deployment (内部版本)
type Deployment struct {
    MaxSurge *int32
}

2. 内部版本不需要新增对应字段的情况

如果新增字段仅对外部使用(如 UI 显示、注解、扩展功能)且不影响核心逻辑,内部版本可以不新增该字段:

  • 注解方式保留 :外部版本的新增字段可以转换为注解,存储在 ObjectMeta.Annotations 中。
  • 忽略逻辑处理:在转换到内部版本时丢弃字段,在从内部版本转换回外部版本时补充默认值或从注解恢复。
示例:someFeatureEnabled
  • 假设外部版本新增字段 someFeatureEnabled,仅用于用户配置,不影响控制器逻辑。
  • 内部版本无需新增字段,可以通过注解处理。

代码示例:

go 复制代码
// apps/v1.Deployment (外部版本)
type Deployment struct {
    SomeFeatureEnabled bool
}

// apps.Deployment (内部版本)
type Deployment struct {
    // 不需要新增字段
}

// 转换逻辑
func Convert_v1_Deployment_to_apps_Deployment(in *v1.Deployment, out *apps.Deployment, s conversion.Scope) error {
    if in.SomeFeatureEnabled {
        if out.Annotations == nil {
            out.Annotations = make(map[string]string)
        }
        out.Annotations["someFeatureEnabled"] = "true"
    }
    return nil
}

func Convert_apps_Deployment_to_v1_Deployment(in *apps.Deployment, out *v1.Deployment, s conversion.Scope) error {
    if val, ok := in.Annotations["someFeatureEnabled"]; ok && val == "true" {
        out.SomeFeatureEnabled = true
    }
    return nil
}

3. 新增字段需要的关键考虑

(1) 是否影响核心逻辑
  • :需要新增到内部版本。
  • :可以通过注解或默认值处理。
(2) 转换过程中的数据完整性
  • 如果外部版本新增的字段需要在多个外部版本之间保持一致,最好在内部版本中新增字段。
  • 如果字段仅用于展示或配置,转换到内部版本时可以省略。
(3) 新字段是否向后兼容
  • 在未升级到新外部版本的情况下,内部版本是否还能正常工作?新增字段是否需要兼容旧逻辑?

4. 总结

  • 外部版本新增字段时,不一定需要在内部版本中新增对应字段。

  • 需要新增的情况

    • 新字段直接影响 Kubernetes 核心逻辑。
    • 新字段需要在版本间保持一致性,避免丢失数据。
  • 不需要新增的情况

    • 新字段仅用于外部展示或扩展功能。
    • 新字段可以通过注解或默认值处理,且不影响核心逻辑。
相关推荐
TC13981 小时前
docker 终端打不开rviz2界面,报错qt.qpa.xcb: could not connect to display
docker·容器
扣脚大汉在网络3 小时前
云原生安全渗透篇
安全·云原生·dubbo
csdn_aspnet4 小时前
使用 .NET 9 和 Azure 构建云原生应用程序:有什么新功能?
microsoft·云原生·azure
字节源流5 小时前
【spring cloud Netflix】Eureka注册中心
云原生·eureka
Brilliant Nemo5 小时前
Docker 镜像相关的基本操作
运维·docker·容器
基哥的奋斗历程6 小时前
kubernetes configMap 存储
云原生·容器·kubernetes
阿里云云原生1 天前
LLM 不断提升智能下限,MCP 不断提升创意上限
云原生
阿里云云原生1 天前
GraalVM 24 正式发布阿里巴巴贡献重要特性 —— 支持 Java Agent 插桩
云原生
云上艺旅1 天前
K8S学习之基础七十四:部署在线书店bookinfo
学习·云原生·容器·kubernetes