2024年4月

引言

上一篇我们创建了一个
Sample.Api
项目和
Sample.Repository
,并且带大家熟悉了一下
Moq
的概念,这一章我们来实战一下在
xUnit
项目使用依赖注入。

Xunit.DependencyInjection

Xunit.DependencyInjection
是一个用于
xUnit
测试框架的扩展库,它提供了依赖注入的功能,使得在编写单元测试时可以更方便地进行依赖注入。通过使用
Xunit.DependencyInjection
,可以在
xUnit
测试中使用依赖注入容器(比如
Microsoft.Extensions.DependencyInjection
)来管理测试中所需的各种依赖关系,包括服务、日志、配置等等。

使用

我们用
Xunit.DependencyInjection
对上一章的
Sample.Repository
进行单元测试。

Nuget
包安装项目依赖

PM> NuGet\Install-Package Xunit.DependencyInjection -Version 9.1.0

创建测试类

public class StaffRepositoryTest
{
    [Fact]
    public void DependencyInject_WhenCalled_ReturnTrue()
    {
        Assert.True(true);
    }
}

运行测试 先看一下

image

从这可以得出一个结论 如果安装了
Xunit.DependencyInjection

xUnit
单元测试项目启动时会检测是否有默认的
Startup

如果你安装了
Xunit.DependencyInjection
但是还没有准备好在项目中使用也可以在
csproj
中禁用

<Project>
    <PropertyGroup>
        <EnableXunitDependencyInjectionDefaultTestFrameworkAttribute>false</EnableXunitDependencyInjectionDefaultTestFrameworkAttribute>
    </PropertyGroup>
</Project>

再测试一下

image

可以看到我们添加的配置生效了

配置

在我们的测试项目中新建
Startup.cs

public class Startup
{

}


.Net 6
之前我们不就是用这个来配置项目的依赖和管道吗,其实这个位置也一样用它来对我们项目的依赖和服务做一些基础配置,使用配置单元测试的
Startup
其实和配置我们的
Asp.Net Core
的启动配置是一样的

CreateHostBuilder

CreateHostBuilder
方法用于创建应用程序的主机构建器(
HostBuilder
)。在这个方法中,您可以配置主机的各种参数、服务、日志、环境等。这个方法通常用于配置主机构建器的各种属性,以便在应用程序启动时使用。

public IHostBuilder CreateHostBuilder([AssemblyName assemblyName]) { }

ConfigureHost

ConfigureHost
方法用于配置主机构建器。在这个方法中,您可以对主机进行一些自定义的配置,比如设置环境、使用特定的配置源等

  public void ConfigureHost(IHostBuilder hostBuilder) { }

ConfigureServices

ConfigureServices
方法用于配置依赖注入容器(
ServiceCollection
)。在这个方法中,您可以注册应用程序所需的各种服务、中间件、日志、数据库上下文等等。这个方法通常用于配置应用程序的依赖注入服务。

Configure

ConfigureServices
中配置的服务可以在
Configure
方法中指定。如果已经配置的服务在 Configure 方法的参数中可用,它们将会被注入

    public void Configure()
    {

    }

Sample.Repository

接下来对我们的仓储层进行单元测试
已知我们的仓储层已经有注入的扩展方法

    public static IServiceCollection AddEFCoreInMemoryAndRepository(this IServiceCollection services)
    {
        services.AddScoped<IStaffRepository, StaffRepository>();
        services.AddDbContext<SampleDbContext>(options => options.UseInMemoryDatabase("sample").EnableSensitiveDataLogging(), ServiceLifetime.Scoped);
        return services;
    }

所以我们只需要在单元测试项目的
Startup

ConfigureServices
注入即可。
对我们的
Sample.Repository
添加项目引用,然后进行依赖注册

    public void ConfigureServices(IServiceCollection services, HostBuilderContext context)
    {
        services.AddEFCoreInMemoryAndRepository();
    }

好了接下来编写单元测试
Case

依赖项获取:

public class StaffRepositoryTest
{
    private readonly IStaffRepository _staffRepository;
    public StaffRepositoryTest(IStaffRepository staffRepository)
    {
        _staffRepository = staffRepository;
    }
}

在测试类中使用依赖注入和我们正常获取依赖是一样的都是通过构造函数的形式

 public class StaffRepositoryTest
{
    private readonly IStaffRepository _staffRepository;
    public StaffRepositoryTest(IStaffRepository staffRepository)
    {
        _staffRepository = staffRepository;
    }

    //[Fact]
    //public void DependencyInject_WhenCalled_ReturnTrue()
    //{
    //    Assert.True(true);
    //}

    [Fact]
    public async Task AddStaffAsync_WhenCalled_ShouldAddStaffToDatabase()
    {
        // Arrange
        var staff = new Staff { Name = "zhangsan", Email = "zhangsan@163.com" };
        // Act
        await _staffRepository.AddStaffAsync(staff, CancellationToken.None);
        // Assert
        var retrievedStaff = await _staffRepository.GetStaffByIdAsync(staff.Id, CancellationToken.None);
        Assert.NotNull(retrievedStaff); // 确保 Staff 已成功添加到数据库
        Assert.Equal("zhangsan", retrievedStaff.Name); // 检查名称是否正确
    }


    [Fact]
    public async Task DeleteStaffAsync_WhenCalled_ShouldDeleteStaffFromDatabase()
    {

        var staff = new Staff { Name = "John", Email = "john@example.com" };
        await _staffRepository.AddStaffAsync(staff, CancellationToken.None); // 先添加一个 Staff

        // Act
        await _staffRepository.DeleteStaffAsync(staff.Id, CancellationToken.None); // 删除该 Staff

        // Assert
        var retrievedStaff = await _staffRepository.GetStaffByIdAsync(staff.Id, CancellationToken.None); // 尝试获取已删除的 Staff
        Assert.Null(retrievedStaff); // 确保已经删除

    }


    [Fact]
    public async Task UpdateStaffAsync_WhenCalled_ShouldUpdateStaffInDatabase()
    {
        // Arrange
        var staff = new Staff { Name = "John", Email = "john@example.com" };
        await _staffRepository.AddStaffAsync(staff, CancellationToken.None); // 先添加一个 Staff

        // Act
        staff.Name = "Updated Name";
        await _staffRepository.UpdateStaffAsync(staff, CancellationToken.None); // 更新 Staff

        // Assert
        var updatedStaff = await _staffRepository.GetStaffByIdAsync(staff.Id, CancellationToken.None); // 获取已更新的 Staff
        Assert.Equal("Updated Name", updatedStaff?.Name); // 确保 Staff 已更新

    }

    [Fact]
    public async Task GetStaffByIdAsync_WhenCalledWithValidId_ShouldReturnStaffFromDatabase()
    {
        // Arrange
        var staff = new Staff { Name = "John", Email = "john@example.com" };
        await _staffRepository.AddStaffAsync(staff, CancellationToken.None); // 先添加一个 Staff
                                                                             // Act
        var retrievedStaff = await _staffRepository.GetStaffByIdAsync(staff.Id, CancellationToken.None); // 获取 Staff
                                                                                                         // Assert
        Assert.NotNull(retrievedStaff); // 确保成功获取 Staff

    }

    [Fact]
    public async Task GetAllStaffAsync_WhenCalled_ShouldReturnAllStaffFromDatabase()
    {
        // Arrange
        var staff1 = new Staff { Name = "John", Email = "john@example.com" };
        var staff2 = new Staff { Name = "Alice", Email = "alice@example.com" };
        await _staffRepository.AddStaffAsync(staff1, CancellationToken.None); // 先添加 Staff1
        await _staffRepository.AddStaffAsync(staff2, CancellationToken.None); // 再添加 Staff2

        // Act
        var allStaff = await _staffRepository.GetAllStaffAsync(CancellationToken.None); // 获取所有 Staff

        // Assert
        List<Staff> addStaffs = [staff1, staff2];
        Assert.True(addStaffs.All(_ => allStaff.Any(x => x.Id == _.Id))); // 确保成功获取所有 Staff
    }
}

Run Tests

image

可以看到单元测试已经都成功了,是不是很简单呢。

扩展

如何注入 ITestOutputHelper?

之前的示例不使用
xUnit.DependencyInjection
我们用
ITestOutputHelper
通过构造函数构造,现在是用
ITestOutputHelperAccessor

public class DependencyInjectionTest
{
    private readonly ITestOutputHelperAccessor _testOutputHelperAccessor;
    public DependencyInjectionTest(ITestOutputHelperAccessor testOutputHelperAccessor)
    {
        _testOutputHelperAccessor = testOutputHelperAccessor;
    }

    [Fact]
    public void TestOutPut_Console()
    {
        _testOutputHelperAccessor.Output?.WriteLine("测试ITestOutputHelperAccessor");
        Assert.True(true);
    }
}

OutPut:

image

日志输出到 ITestOutputHelper

Nuget
安装

PM> NuGet\Install-Package Xunit.DependencyInjection.Logging -Version 9.0.0

ConfigureServices
配置依赖

 public void ConfigureServices(IServiceCollection services)
        => services.AddLogging(lb => lb.AddXunitOutput());

使用:

public class DependencyInjectionTest
{
    private readonly ILogger<DependencyInjectionTest> _logger;
    public DependencyInjectionTest(ILogger<DependencyInjectionTest> logger)
    {
        _logger = logger;
    }

    [Fact]
    public void Test()
    {
        _logger.LogDebug("LogDebug");
        _logger.LogInformation("LogInformation");
        _logger.LogError("LogError");
    }
}

OutPut:

 标准输出: 
[2024-04-12 16:00:24Z] info: dotNetParadise.DependencyInjection.DependencyInjectionTest[0]
      LogInformation
[2024-04-12 16:00:24Z] fail: dotNetParadise.DependencyInjection.DependencyInjectionTest[0]
      LogError

startup 类中注入 IConfiguration 或 IHostEnvironment

通过
ConfigureServices
设置
EnvironmentName
和使用
IConfiguration

   public void ConfigureServices(HostBuilderContext context)
    {
        context.HostingEnvironment.EnvironmentName = "test";
           //使用配置
        context.Configuration.GetChildren();
    }

也可以使用
Startup
下的
ConfigureHost
设置

public class Startup
{
    public void ConfigureHost(IHostBuilder hostBuilder) =>
        hostBuilder
            .ConfigureServices((context, services) => { context.XXXX });
}

在 ConfigureHost 下可以对
.Net
IHostBuilder
进行配置,可以对
IConfiguration
,
IServiceCollection
,
Log
等跟
Asp.Net Core
使用一致。

集成测试

xUnit.DependencyInjection
也可以对
Asp.Net Core
项目进行集成测试

安装
Microsoft.AspNetCore.TestHost

PM> NuGet\Install-Package Microsoft.AspNetCore.TestHost -Version 9.0.0-preview.3.24172.13
    public void ConfigureHost(IHostBuilder hostBuilder) =>
        hostBuilder.ConfigureWebHost[Defaults](webHostBuilder => webHostBuilder
            .UseTestServer(options => options.PreserveExecutionContext = true)
            .UseStartup<AspNetCoreStartup>());

可以参考 xUnit 的官网实现,其实有更优雅的实现集成测试的方案,
xUnit.DependencyInject
的集成测试方案仅做参考集合,在后面章节笔者会对集成测试做详细的介绍。

最后

希望本文对您在使用
Xunit.DependencyInjection
进行依赖注入和编写单元测试时有所帮助。通过本文的介绍,您可以更加灵活地管理测试项目中的依赖关系,提高测试代码的可维护性和可测试性


在 Avalonia 中,样式是定义控件外观的一种方式,而控件主题则是一组样式和资源,用于定义应用程序的整体外观和感觉。本文将深入探讨这些概念,并提供示例代码以帮助您更好地理解它们。

样式是什么?

样式是一组属性,用于定义控件的外观。它们可以包括背景色、边框、字体样式等。在 Avalonia 中,样式通常以 XAML 格式定义,并应用于特定的控件。

<StackPanel>
  <StackPanel.Styles>
    <StyleSelector="Border:pointerover">
      <SetterProperty="Background"Value="Red"/>
    </Style>
  </StackPanel.Styles>
  <Border>
    <TextBlock>I will have red background when hovered.</TextBlock>
  </Border>
</StackPanel>

此示例中:pointerover 伪类表示指针输入当前悬停在控件上(在控件的边界内)。(这个伪类类似于 CSS 中的 :hover。)

样式类是什么?

样式类是一种将样式应用于控件的方法。它们允许您在多个控件之间共享样式,并提高代码的可维护性。通过将样式定义为样式类,您可以轻松地将其应用于多个控件,而无需重复定义样式。

以下是一个示例,展示如何在 Avalonia 中定义和应用样式类:

<Window.Styles>
    <StyleSelector="TextBlock.h1">
        <SetterProperty="FontSize"Value="24"/>
        <SetterProperty="FontWeight"Value="Bold"/>
    </Style>
</Window.Styles>
<StackPanelMargin="20">
    <TextBlockClasses="h1">Heading 1</TextBlock>
</StackPanel>    

在此示例中,所有带有 h1 样式类的 TextBlock 元素将显示为样式设置的字体大小和字重。

控件主题是什么?

控件主题是一组样式和资源,用于定义应用程序的整体外观和感觉。它们允许您轻松地更改应用程序的外观,而无需修改每个控件的样式。控件主题通常包含全局样式、颜色方案和字体设置等。

以下是一个示例,展示如何在 Avalonia 中定义和应用控件主题:

App.axaml

<Application.Resources>
    <ControlThemex:Key="EllipseButton"TargetType="Button">
        <SetterProperty="Background"Value="Blue"/>
        <SetterProperty="Foreground"Value="Yellow"/>
        <SetterProperty="Padding"Value="8"/>
        <SetterProperty="Template">
            <ControlTemplate>
                <Panel>
                    <EllipseFill="{TemplateBinding Background}"HorizontalAlignment="Stretch"VerticalAlignment="Stretch"/>
                    <ContentPresenterx:Name="PART_ContentPresenter"Content="{TemplateBinding Content}"Margin="{TemplateBinding Padding}"/>
                </Panel>
            </ControlTemplate>
        </Setter>
    </ControlTheme>
</Application.Resources>

MainWindow.axaml

<ButtonTheme="{StaticResource EllipseButton}"HorizontalAlignment="Center"VerticalAlignment="Center">Hello World!</Button>

通过这些示例,您现在应该对在 Avalonia 中使用样式和控件主题有了更好的理解。样式类和控件主题使得管理和修改应用程序的外观变得更加简单和灵活。

Redis Stream 是 Redis 5.0 版本中引入的一种新的数据结构,它用于实现简单但功能强大的消息传递模式。

这篇文章,我们聊聊 Redis Stream 基本用法 ,以及如何在 SpringBoot 项目中应用 Redis Stream 。

1 基础知识

Redis Stream 的结构如下图所示,它是一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容。

每个 Redis Stream 都有唯一的名称 ,对应唯一的 Redis Key 。

同一个 Stream 可以挂载多个
消费组 ConsumerGroup
, 消费组不能自动创建,需要
使用 XGROUP CREATE 命令创建

每个消费组会有个
游标 last_delivered_id
,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动 ,标识当前消费组消费到哪条消息了。

消费组 ConsumerGroup 同样可以挂载多个消费者 Consumer , 每个 Consumer 并行的读取消息,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。

消费者内部有一个属性
pending_ids
, 记录了当前消费者读取但没有回复 ACK 的消息 ID 列表 。

2 核心命令

01 XADD 向 Stream 末尾添加消息

使用 XADD 向队列添加消息,如果指定的队列不存在,则创建一个队列。基础语法格式:

XADD key ID field value [field value ...]
  • key
    :队列名称,如果不存在就创建
  • ID
    :消息 id,我们使用 * 表示由 redis 生成,可以自定义,但是要自己保证递增性。
  • field value
    : 记录。
127.0.0.1:6379> XADD mystream * name1 value1 name2 value2
"1712473185388-0"
127.0.0.1:6379> XLEN mystream
(integer) 1
127.0.0.1:6379> XADD mystream * name2 value2 name3 value3
"1712473231761-0"

消息 ID 使用 * 表示由 redis 生成,同时也可以自定义,但是自定义时要保证递增性。

消息 ID 的格式: 毫秒级时间戳 + 序号 , 例如:1712473185388-5 , 它表示当前消息在毫秒时间戳 1712473185388 产生 ,并且该毫秒内产生到了第5条消息。

在添加队列消息时,也
可以指定队列的长度

127.0.0.1:6379> XADD mystream MAXLEN 100 * name value1 age 30
"1713082205042-0"

使用 XADD 命令向
mystream
的 stream 中添加了一条消息,并且指定了最大长度为 100。消息的 ID 由 Redis 自动生成,消息包含两个字段
name

age
,分别对应的值是
value1

30

02 XRANGE 获取消息列表

使用 XRANGE 获取消息列表,会自动过滤已经删除的消息。语法格式:

XRANGE key start end [COUNT count]
  • key
    :队列名
  • start
    :开始值,
    -
    表示最小值
  • end
    :结束值,
    +
    表示最大值
  • count
    :数量
127.0.0.1:6379> XRANGE mystream - + COUNT 2
1) 1) "1712473185388-0"
   2) 1) "name1"
      2) "value1"
      3) "name2"
      4) "value2"
2) 1) "1712473231761-0"
   2) 1) "name2"
      2) "value2"
      3) "name3"
      4) "value3"

我们得到两条消息,第一层是消息 ID ,第二层是消息内容 ,消息内容是 Hash 数据结构 。

03 XREAD 以阻塞/非阻塞方式获取消息列表

使用 XREAD 以阻塞或非阻塞方式获取消息列表 ,语法格式:

XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]
  • count
    :数量
  • milliseconds
    :可选,阻塞毫秒数,没有设置就是非阻塞模式
  • key
    :队列名
  • id
    :消息 ID
127.0.0.1:6379> XREAD streams mystream 0-0
1) 1) "mystream"
   2) 1) 1) "1712473185388-0"
         2) 1) "name1"
            2) "value1"
            3) "name2"
            4) "value2"
      2) 1) "1712473231761-0"
         2) 1) "name2"
            2) "value2"
            3) "name3"
            4) "value3"

XRED 读消息时分为
阻塞

非阻塞
模式,使用
BLOCK
选项可以表示阻塞模式,需要设置阻塞时长。非阻塞模式下,读取完毕(即使没有任何消息)立即返回,而在阻塞模式下,若读取不到内容,则阻塞等待。

127.0.0.1:6379> XREAD block 1000 streams mystream $
(nil)
(1.07s)

使用 Block 模式,配合 $ 作为 ID ,表示读取最新的消息,若没有消息,命令阻塞!等待过程中,其他客户端向队列追加消息,则会立即读取到。

因此,典型的队列就是 XADD 配合 XREAD Block 完成。XADD 负责生成消息,XREAD 负责消费消息。

04 XGROUP CREATE 创建消费者组

使用 XGROUP CREATE 创建消费者组,分两种情况:

  • 从头开始消费:
XGROUP CREATE mystream consumer-group-name 0-0  
  • 从尾部开始消费:
XGROUP CREATE mystream consumer-group-name $

执行效果如下:

127.0.0.1:6379> XGROUP CREATE mystream mygroup 0-0
OK

05 XREADGROUP GROUP 读取消费组中的消息

使用 XREADGROUP GROUP 读取消费组中的消息,语法格式:

XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
  • group
    :消费组名
  • consumer
    :消费者名。
  • count
    : 读取数量。
  • milliseconds
    : 阻塞毫秒数。
  • key
    : 队列名。
  • ID
    : 消息 ID。

示例:

127.0.0.1:6379>  XREADGROUP group mygroup consumerA count 1 streams mystream >
1) 1) "mystream"
   2) 1) 1) "1712473185388-0"
         2) 1) "name1"
            2) "value1"
            3) "name2"
            4) "value2"

消费者组
mygroup
中的消费者
consumerA
,从 名为
mystream
的 Stream 中读取消息。

  • COUNT 1
    表示一次最多读取一条消息
  • >
    表示消息的起始位置是当前可用消息的 ID,即从当前未读取的最早消息开始读取。

06 XACK 消息消费确认

接收到消息之后,我们要手动确认一下(ack),语法格式:

xack key group-key ID [ID ...]

示例:

127.0.0.1:6379> XACK mystream mygroup 1713089061658-0
(integer) 1

消费确认增加了消息的可靠性,一般在业务处理完成之后,需要执行 ack 确认消息已经被消费完成,整个流程的执行如下图所示:

我们可以使用 xpending 命令查看
消费者未确认的消息ID

127.0.0.1:6379> xpending mystream mygroup
1) (integer) 1
2) "1713091227595-0"
3) "1713091227595-0"
4) 1) 1) "consumerA"
      2) "1"

07 XTRIM 限制 Stream 长度

我们使用 XTRIM 对流进行修剪,限制长度, 语法格式:

127.0.0.1:6379> XADD mystream * field1 A field2 B field3 C field4 D
"1712535017402-0"
127.0.0.1:6379> XTRIM mystream MAXLEN 2
(integer) 4
127.0.0.1:6379> XRANGE mystream - +
1) 1) "1712498239430-0"
   2) 1) "name"
      2) "zhangyogn"
2) 1) "1712535017402-0"
   2) 1) "field1"
      2) "A"
      3) "field2"
      4) "B"
      5) "field3"
      6) "C"
      7) "field4"
      8) "D"

3 SpringBoot Redis Stream 实战

1、添加 SpringBoot Redis 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、yaml 文件配置

3、RedisTemplate 配置

4、定义stream监听器

5、定义streamcontainer 并启动

6、发送消息

执行完成之后,消费者就可以打印如下日志:

演示代码地址:

https://github.com/makemyownlife/courage-cache-demo

4 Redis stream 用做消息队列完美吗

笔者认为 Redis stream 用于消息队列最大的进步在于:
实现了发布订阅模型

发布订阅模型具有如下特点:

  • 消费独立

    相比队列模型的匿名消费方式,发布订阅模型中消费方都会具备的身份,一般叫做订阅组(订阅关系),不同订阅组之间相互独立不会相互影响。

  • 一对多通信

    基于独立身份的设计,同一个主题内的消息可以被多个订阅组处理,每个订阅组都可以拿到全量消息。因此发布订阅模型可以实现一对多通信。

细品 Redis stream 的设计,我们发现它和 Kafka 非常相似,比如说消费者组,消费进度偏移量等。

我们曾经诟病 Redis List 数据结构用做队列时,因为消费时没有 Ack 机制,应用异常挂掉导致消息偶发丢失的情况,Redis Stream 已经完美的解决了。

因为消费者内部有一个属性
pending_ids
, 记录了当前消费者读取但没有回复 ACK 的消息 ID 列表 。当消费者重新上线,这些消息可以重新被消费。

但 Redis stream 用做消息队列完美吗 ?

这个真没有!

1、Redis 本身定位是
内存数据库
,它的设计之初都是为缓存准备的,
并不具备消息堆积的能力
。而专业消息队列一个非常重要的功能是
数据中转枢纽
,Redis 的定位很难满足,所以使用起来要非常小心。

2、Redis 的高可用方案可能丢失消息(AOF 持久化 和 主从复制都是异步 ),而专业消息队列可以针对不同的场景选择不同的高可用策略。

所以,笔者认为 Redis 非常适合轻量级消息队列解决方案,轻量级意味着:数据量可控 + 业务模型简单 。


参考文章:

https://redis.io/docs/data-types/streams/

https://www.runoob.com/redis/redis-stream.html

https://pdai.tech/md/db/nosql-redis/db-redis-data-type-stream.html


笔者开源项目推荐:

简单易用的短信服务:
https://github.com/makemyownlife/platform-sms

分库分表实战演示:
https://github.com/makemyownlife/shardingsphere-jdbc-demo

如果我的文章对你有所帮助,还请帮忙
点赞、在看、转发
一下,你的支持会激励我输出更高质量的文章,非常感谢!

本文分享自华为云社区《
K8s 镜像缓存管理 kube-fledged 认知
》,作者: 山河已无恙。

我们知道
k8s
上的容器调度需要在调度的节点行拉取当前容器的镜像,在一些特殊场景中,

  • 需要
    快速启动和/或扩展
    的应用程序。例如,由于数据量激增,执行实时数据处理的应用程序需要快速扩展。
  • 镜像比较庞大,涉及多个版本,节点存储有限,需要动态清理不需要的镜像
  • 无服务器函数
    通常需要在几分之一秒内立即对传入事件和启动容器做出反应。
  • 在边缘设备上运行的
    IoT 应用程序
    ,需要容忍
    边缘设备
    和镜像镜像仓库之间的间歇性网络连接。
  • 如果需要从
    专用仓库
    中拉取镜像,并且无法授予每个人从此
    镜像仓库
    拉取镜像的访问权限,则可以在群集的节点上提供镜像。
  • 如果集群管理员或操作员需要对应用程序进行升级,并希望事先验证是否可以成功拉取新镜像。

kube-fledged
是一个
kubernetes operator
,用于直接在 Kubernetes 集群的
worker
节点上创建和管理容器镜像缓存。它允许用户定义镜像列表以及这些镜像应缓存到哪些工作节点上(即拉取)。因此,应用程序 Pod 几乎可以立即启动,因为不需要从镜像仓库中提取镜像。

kube-fledged
提供了 CRUD API 来管理镜像缓存的生命周期,并支持多个可配置的参数,可以根据自己的需要自定义功能。

Kubernetes 具有内置的
镜像垃圾回收机制
。节点中的 kubelet 会定期检查磁盘使用率是否达到特定阈值(可通过标志进行配置)。一旦达到这个
阈值
,kubelet 会自动删除节点中所有未使用的镜像。

需要在建议的解决方案中实现自动和定期刷新机制。如果镜像缓存中的镜像被 kubelet 的 gc 删除,下一个刷新周期会将已删除的镜像拉入镜像缓存中。这可确保镜像缓存是最新的。

设计流程

https://github.com/senthilrch/kube-fledged/blob/master/docs/kubefledged-architecture.png

部署 kube-fledged

Helm 方式部署

──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$mkdir kube
-fledged
┌──[root@vms100.liruilongs.github.io]
-[~/ansible]
└─$cd kube
-fledged
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged]
└─$export KUBEFLEDGED_NAMESPACE
=kube-fledged
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged]
└─$kubectl create
namespace${KUBEFLEDGED_NAMESPACE}namespace/kube-fledged created
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged]
└─$helm repo add kubefledged
-charts https://senthilrch.github.io/kubefledged-charts/ "kubefledged-charts"has been added to your repositories
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged]
└─$helm repo update
Hang tight
while we grab the latest fromyour chart repositories...
...Successfully got an update
from the "kubefledged-charts"chart repository
...Successfully got an update
from the "kubescape"chart repository
...Successfully got an update
from the "rancher-stable"chart repository
...Successfully got an update
from the "skm"chart repository
...Successfully got an update
from the "openkruise"chart repository
...Successfully got an update
from the "awx-operator"chart repository
...Successfully got an update
from the "botkube"chart repository
Update Complete. ⎈Happy Helming
!
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged]
└─$helm install
--verify kube-fledged kubefledged-charts/kube-fledged -n ${KUBEFLEDGED_NAMESPACE} --wait

实际部署中发现,由于网络问题,
chart
无法下载,所以通过
make deploy-using-yaml
使用 yaml 方式部署

Yaml 文件部署

┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged]
└─$git clone https:
//github.com/senthilrch/kube-fledged.git 正克隆到 'kube-fledged'...
remote: Enumerating objects:
10613, done.
remote: Counting objects:
100% (1501/1501), done.
remote: Compressing objects:
100% (629/629), done.
remote: Total
10613 (delta 845), reused 1357 (delta 766), pack-reused 9112接收对象中:100% (10613/10613), 34.58 MiB | 7.33 MiB/s, done.
处理 delta 中:
100% (4431/4431), done.
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged]
└─$ls
kube
-fledged
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged]
└─$cd kube
-fledged/┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged/kube-fledged]
└─$make deploy
-using-yaml
kubectl apply
-f deploy/kubefledged-namespace.yaml

第一次部署,发现镜像拉不下来

┌──[root@vms100.liruilongs.github.io]-[~]
└─$kubectl
get all -n kube-fledged
NAME READY STATUS RESTARTS AGE
pod
/kube-fledged-controller-df69f6565-drrqg 0/1 CrashLoopBackOff 35(5h59m ago) 21h
pod
/kube-fledged-webhook-server-7bcd589bc4-b7kg2 0/1 Init:CrashLoopBackOff 35(5h58m ago) 21h
pod
/kubefledged-controller-55f848cc67-7f4rl 1/1 Running 021h
pod
/kubefledged-webhook-server-597dbf4ff5-l8fbh 0/1 Init:CrashLoopBackOff 34(6h ago) 21h

NAME TYPE CLUSTER
-IP EXTERNAL-IP PORT(S) AGE
service
/kube-fledged-webhook-server ClusterIP 10.100.194.199 <none> 3443/TCP 21h
service
/kubefledged-webhook-server ClusterIP 10.101.191.206 <none> 3443/TCP 21h

NAME READY UP
-TO-DATE AVAILABLE AGE
deployment.apps
/kube-fledged-controller 0/1 1 021h
deployment.apps
/kube-fledged-webhook-server 0/1 1 021h
deployment.apps
/kubefledged-controller 0/1 1 021h
deployment.apps
/kubefledged-webhook-server 0/1 1 021h

NAME DESIRED CURRENT READY AGE
replicaset.apps
/kube-fledged-controller-df69f6565 1 1 021h
replicaset.apps
/kube-fledged-webhook-server-7bcd589bc4 1 1 021h
replicaset.apps
/kubefledged-controller-55f848cc67 1 1 021h
replicaset.apps
/kubefledged-webhook-server-597dbf4ff5 1 1 021h
┌──[root@vms100.liruilongs.github.io]
-[~]
└─$

这里我们找一下要拉取的镜像

┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$cat
*.yaml |grep image:- image: senthilrch/kubefledged-controller:v0.10.0 - image: senthilrch/kubefledged-webhook-server:v0.10.0 - image: senthilrch/kubefledged-webhook-server:v0.10.0

单独拉取一些,当前使用
ansible
在所有工作节点批量操作

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$ansible k8s_node
-m shell -a "docker pull docker.io/senthilrch/kubefledged-cri-client:v0.10.0" -i host.yaml

其他相关的镜像都拉取一下

操作完成之后容器状态全部正常

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$kubectl
-n kube-fledged getall
NAME READY STATUS RESTARTS AGE
pod
/kube-fledged-controller-df69f6565-wdb4g 1/1 Running 013h
pod
/kube-fledged-webhook-server-7bcd589bc4-j8xxp 1/1 Running 013h
pod
/kubefledged-controller-55f848cc67-klxlm 1/1 Running 013h
pod
/kubefledged-webhook-server-597dbf4ff5-ktbsh 1/1 Running 013h

NAME TYPE CLUSTER
-IP EXTERNAL-IP PORT(S) AGE
service
/kube-fledged-webhook-server ClusterIP 10.100.194.199 <none> 3443/TCP 36h
service
/kubefledged-webhook-server ClusterIP 10.101.191.206 <none> 3443/TCP 36h

NAME READY UP
-TO-DATE AVAILABLE AGE
deployment.apps
/kube-fledged-controller 1/1 1 136h
deployment.apps
/kube-fledged-webhook-server 1/1 1 136h
deployment.apps
/kubefledged-controller 1/1 1 136h
deployment.apps
/kubefledged-webhook-server 1/1 1 136h

NAME DESIRED CURRENT READY AGE
replicaset.apps
/kube-fledged-controller-df69f6565 1 1 136h
replicaset.apps
/kube-fledged-webhook-server-7bcd589bc4 1 1 136h
replicaset.apps
/kubefledged-controller-55f848cc67 1 1 136h
replicaset.apps
/kubefledged-webhook-server-597dbf4ff5 1 1 1 36h

验证是否安装成功

┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged/kube-fledged]
└─$kubectl
get pods -n kube-fledged -l app=kubefledged
NAME READY STATUS RESTARTS AGE
kubefledged
-controller-55f848cc67-klxlm 1/1 Running 016h
kubefledged
-webhook-server-597dbf4ff5-ktbsh 1/1 Running 016h
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged/kube-fledged]
└─$kubectl
get imagecaches -n kube-fledged
No resources found
in kube-fledged namespace.

使用 kubefledged

创建镜像缓存对象

根据
Demo
文件,创建镜像缓存对象

┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged/kube-fledged]
└─$cd deploy
/┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$cat kubefledged
-imagecache.yaml---apiVersion: kubefledged.io/v1alpha2
kind: ImageCache
metadata:
# Name of the image cache. A cluster can have multiple image cache objects
name: imagecache1
namespace: kube-fledged
# The kubernetes
namespace to be used for this image cache. You can choose a different namepace asper your preference
labels:
app: kubefledged
kubefledged: imagecache
spec:
# The
"cacheSpec" field allows a user to define a list of images and onto which worker nodes those images should be cached (i.e. pre-pulled).
cacheSpec:
# Specifies a list of images (nginx:
1.23.1) with no node selector, hence these images will be cached in all the nodes inthe cluster-images:- ghcr.io/jitesoft/nginx:1.23.1# Specifies a list of images (cassandra:v7 and etcd:3.5.4-0) with a node selector, hence these images will be cached only on the nodes selected by the node selector-images:- us.gcr.io/k8s-artifacts-prod/cassandra:v7- us.gcr.io/k8s-artifacts-prod/etcd:3.5.4-0nodeSelector:
tier: backend
# Specifies a list of image pull secrets to pull images
from privaterepositories into the cache
imagePullSecrets:
- name: myregistrykey

官方的 Demo 中对应的 镜像拉取不下来,所以换一下

┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$docker pull us.gcr.io
/k8s-artifacts-prod/cassandra:v7
Error response
from daemon: Get "https://us.gcr.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded whileawaiting headers)
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$

为了测试选择器标签的使用,我们找一个节点的标签单独做镜像缓存

┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$kubectl
get nodes --show-labels

同时我们直接从公有仓库拉取镜像,所以不需要
imagePullSecrets
对象

┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$vim kubefledged
-imagecache.yaml

修改后的
yaml
文件

  • 添加了一个所有节点的 liruilong/my-busybox:latest 镜像缓存
  • 添加了一个
    kubernetes.io/hostname: vms105.liruilongs.github.io
    对应标签选择器的
    liruilong/hikvision-sdk-config-ftp:latest
    镜像缓存
┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$cat kubefledged
-imagecache.yaml---apiVersion: kubefledged.io/v1alpha2
kind: ImageCache
metadata:
# Name of the image cache. A cluster can have multiple image cache objects
name: imagecache1
namespace: kube-fledged
# The kubernetes
namespace to be used for this image cache. You can choose a different namepace asper your preference
labels:
app: kubefledged
kubefledged: imagecache
spec:
# The
"cacheSpec" field allows a user to define a list of images and onto which worker nodes those images should be cached (i.e. pre-pulled).
cacheSpec:
# Specifies a list of images (nginx:
1.23.1) with no node selector, hence these images will be cached in all the nodes inthe cluster-images:- liruilong/my-busybox:latest
# Specifies a list of images (cassandra:v7 and etcd:
3.5.4-0) with a node selector, hence these images will be cached only on the nodes selected by the node selector-images:- liruilong/hikvision-sdk-config-ftp:latest
nodeSelector:
kubernetes.io
/hostname: vms105.liruilongs.github.io
# Specifies a list of image pull secrets to pull images
from privaterepositories into the cache
#imagePullSecrets:
#
-name: myregistrykey
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$

直接创建报错了

┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$kubectl create
-f kubefledged-imagecache.yaml
Error
from server (InternalError): error when creating "kubefledged-imagecache.yaml": Internal error occurred: failed calling webhook "validate-image-cache.kubefledged.io": failed to call webhook: Post "https://kubefledged-webhook-server.kube-fledged.svc:3443/validate-image-cache?timeout=1s": x509: certificate signed by unknown authority (possibly because of "crypto/rsa: verification error" while trying to verify candidate authority certificate "kubefledged.io")
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$kubectl
get imagecaches -n kube-fledged
No resources found
in kube-fledged namespace.
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$

解决办法,删除对应的对象,重新创建

我在当前项目的一个
issues
下面找到了解决办法
https://github.com/senthilrch/kube-fledged/issues/76

看起来这是因为
Webhook CA
是硬编码的,但是当
webhook
服务器启动时,会生成一个新的 CA 捆绑包并更新 webhook 配置。当发生另一个部署时,将重新应用原始 CA 捆绑包,并且 Webhook 请求开始失败,直到再次重新启动 Webhook 组件以修补捆绑包init-server

┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged/kube-fledged]
└─$make remove
-kubefledged-and-operator# Remove kubefledged
kubectl delete
-f deploy/kubefledged-operator/deploy/crds/charts.helm.kubefledged.io_v1alpha2_kubefledged_cr.yaml
error: resource mapping not found
for name: "kube-fledged" namespace: "kube-fledged" from "deploy/kubefledged-operator/deploy/crds/charts.helm.kubefledged.io_v1alpha2_kubefledged_cr.yaml": no matches for kind "KubeFledged" in version "charts.helm.kubefledged.io/v1alpha2"ensure CRDs are installed first
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged/kube-fledged]
└─$make deploy
-using-yaml
kubectl apply
-f deploy/kubefledged-namespace.yamlnamespace/kube-fledged created
kubectl apply
-f deploy/kubefledged-crd.yaml
customresourcedefinition.apiextensions.k8s.io
/imagecaches.kubefledged.io unchanged
....................
kubectl rollout status deployment kubefledged
-webhook-server -n kube-fledged --watch
Waiting
for deployment "kubefledged-webhook-server" rollout to finish: 0 of 1updated replicas are available...
deployment
"kubefledged-webhook-server" successfully rolled outkubectlget pods -n kube-fledged
NAME READY STATUS RESTARTS AGE
kubefledged
-controller-55f848cc67-76c4v 1/1 Running 0112s
kubefledged
-webhook-server-597dbf4ff5-56h6z 1/1 Running 0 66s

重新创建缓存对象,创建成功

┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$kubectl create
-f kubefledged-imagecache.yaml
imagecache.kubefledged.io
/imagecache1 created
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$kubectl
get imagecaches -n kube-fledged
NAME AGE
imagecache1 10s
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$

查看当前被纳管的镜像缓存

┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged]
└─$kubectl
get imagecaches imagecache1 -n kube-fledged -o json
{
"apiVersion": "kubefledged.io/v1alpha2","kind": "ImageCache","metadata": {"creationTimestamp": "2024-03-01T15:08:42Z","generation": 83,"labels": {"app": "kubefledged","kubefledged": "imagecache"},"name": "imagecache1","namespace": "kube-fledged","resourceVersion": "20169836","uid": "3a680a57-d8ab-444f-b9c9-4382459c5c72"},"spec": {"cacheSpec": [
{
"images": ["liruilong/my-busybox:latest"]
},
{
"images": ["liruilong/hikvision-sdk-config-ftp:latest"],"nodeSelector": {"kubernetes.io/hostname": "vms105.liruilongs.github.io"}
}
]
},
"status": {"completionTime": "2024-03-02T01:06:47Z","message": "All requested images pulled succesfully to respective nodes","reason": "ImageCacheRefresh","startTime": "2024-03-02T01:05:33Z","status": "Succeeded"}
}
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged]
└─$

通过 ansible 来验证

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$ansible all
-m shell -a "docker images | grep liruilong/my-busybox" -i host.yaml192.168.26.102 | CHANGED | rc=0 >>liruilong/my-busybox latest 497b83a63aad 11 months ago 1.24MB192.168.26.101 | CHANGED | rc=0 >>liruilong/my-busybox latest 497b83a63aad 11 months ago 1.24MB192.168.26.103 | CHANGED | rc=0 >>liruilong/my-busybox latest 497b83a63aad 11 months ago 1.24MB192.168.26.105 | CHANGED | rc=0 >>liruilong/my-busybox latest 497b83a63aad 11 months ago 1.24MB192.168.26.100 | CHANGED | rc=0 >>liruilong/my-busybox latest 497b83a63aad 11 months ago 1.24MB192.168.26.106 | CHANGED | rc=0 >>liruilong/my-busybox latest 497b83a63aad 11 months ago 1.24MB
┌──[root@vms100.liruilongs.github.io]
-[~/ansible]
└─$
┌──[root@vms100.liruilongs.github.io]
-[~/ansible]
└─$ansible all
-m shell -a "docker images | grep liruilong/hikvision-sdk-config-ftp" -i host.yaml192.168.26.102 | FAILED | rc=1 >>non-zero returncode192.168.26.100 | FAILED | rc=1 >>non-zero returncode192.168.26.103 | FAILED | rc=1 >>non-zero returncode192.168.26.105 | CHANGED | rc=0 >>liruilong/hikvision-sdk-config-ftp latest a02cd03b4342 4months ago 830MB192.168.26.101 | FAILED | rc=1 >>non-zero returncode192.168.26.106 | FAILED | rc=1 >>non-zero returncode
┌──[root@vms100.liruilongs.github.io]
-[~/ansible]
└─$

开启自动刷新

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$kubectl annotate imagecaches imagecache1
-n kube-fledged kubefledged.io/refresh-imagecache=imagecache.kubefledged.io/imagecache1 annotated
┌──[root@vms100.liruilongs.github.io]
-[~/ansible]
└─$

添加镜像缓存

添加一个新的镜像缓存

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$kubectl
get imagecaches.kubefledged.io -n kube-fledged imagecache1 -o json
{
"apiVersion": "kubefledged.io/v1alpha2","kind": "ImageCache","metadata": {"creationTimestamp": "2024-03-01T15:08:42Z","generation": 92,"labels": {"app": "kubefledged","kubefledged": "imagecache"},"name": "imagecache1","namespace": "kube-fledged","resourceVersion": "20175233","uid": "3a680a57-d8ab-444f-b9c9-4382459c5c72"},"spec": {"cacheSpec": [
{
"images": ["liruilong/my-busybox:latest","liruilong/jdk1.8_191:latest"]
},
{
"images": ["liruilong/hikvision-sdk-config-ftp:latest"],"nodeSelector": {"kubernetes.io/hostname": "vms105.liruilongs.github.io"}
}
]
},
"status": {"completionTime": "2024-03-02T01:43:32Z","message": "All requested images pulled succesfully to respective nodes","reason": "ImageCacheUpdate","startTime": "2024-03-02T01:40:34Z","status": "Succeeded"}
}
┌──[root@vms100.liruilongs.github.io]
-[~/ansible]
└─$

通过 ansible 确认

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$ansible all
-m shell -a "docker images | grep liruilong/jdk1.8_191" -i host.yaml192.168.26.101 | FAILED | rc=1 >>non-zero returncode192.168.26.100 | FAILED | rc=1 >>non-zero returncode192.168.26.102 | FAILED | rc=1 >>non-zero returncode192.168.26.103 | FAILED | rc=1 >>non-zero returncode192.168.26.105 | FAILED | rc=1 >>non-zero returncode192.168.26.106 | FAILED | rc=1 >>non-zero returncode
┌──[root@vms100.liruilongs.github.io]
-[~/ansible]
└─$ansible all
-m shell -a "docker images | grep liruilong/jdk1.8_191" -i host.yaml192.168.26.101 | CHANGED | rc=0 >>liruilong/jdk1.8_191 latest 17dbd4002a8c 5years ago 170MB192.168.26.102 | CHANGED | rc=0 >>liruilong/jdk1.8_191 latest 17dbd4002a8c 5years ago 170MB192.168.26.100 | CHANGED | rc=0 >>liruilong/jdk1.8_191 latest 17dbd4002a8c 5years ago 170MB192.168.26.103 | CHANGED | rc=0 >>liruilong/jdk1.8_191 latest 17dbd4002a8c 5years ago 170MB192.168.26.105 | CHANGED | rc=0 >>liruilong/jdk1.8_191 latest 17dbd4002a8c 5years ago 170MB192.168.26.106 | CHANGED | rc=0 >>liruilong/jdk1.8_191 latest 17dbd4002a8c 5years ago 170MB
┌──[root@vms100.liruilongs.github.io]
-[~/ansible]
└─$

删除镜像缓存

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$kubectl edit imagecaches imagecache1
-n kube-fledged
imagecache.kubefledged.io
/imagecache1 edited
┌──[root@vms100.liruilongs.github.io]
-[~/ansible]
└─$kubectl
get imagecaches.kubefledged.io -n kube-fledged imagecache1 -o json
{
"apiVersion": "kubefledged.io/v1alpha2","kind": "ImageCache","metadata": {"creationTimestamp": "2024-03-01T15:08:42Z","generation": 94,"labels": {"app": "kubefledged","kubefledged": "imagecache"},"name": "imagecache1","namespace": "kube-fledged","resourceVersion": "20175766","uid": "3a680a57-d8ab-444f-b9c9-4382459c5c72"},"spec": {"cacheSpec": [
{
"images": ["liruilong/jdk1.8_191:latest"]
},
{
"images": ["liruilong/hikvision-sdk-config-ftp:latest"],"nodeSelector": {"kubernetes.io/hostname": "vms105.liruilongs.github.io"}
}
]
},
"status": {"message": "Image cache is being updated. Please view the status after some time","reason": "ImageCacheUpdate","startTime": "2024-03-02T01:48:03Z","status": "Processing"}
}

通过 Ansible 确认,可以看到无论是 mastere 上的节点还是 work 的节点,对应的镜像缓存都被清理

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$ansible all
-m shell -a "docker images | grep liruilong/my-busybox" -i host.yaml192.168.26.102 | CHANGED | rc=0 >>liruilong/my-busybox latest 497b83a63aad 11 months ago 1.24MB192.168.26.101 | CHANGED | rc=0 >>liruilong/my-busybox latest 497b83a63aad 11 months ago 1.24MB192.168.26.105 | FAILED | rc=1 >>non-zero returncode192.168.26.100 | CHANGED | rc=0 >>liruilong/my-busybox latest 497b83a63aad 11 months ago 1.24MB192.168.26.103 | FAILED | rc=1 >>non-zero returncode192.168.26.106 | FAILED | rc=1 >>non-zero returncode
┌──[root@vms100.liruilongs.github.io]
-[~/ansible]
└─$ansible all
-m shell -a "docker images | grep liruilong/my-busybox" -i host.yaml192.168.26.105 | FAILED | rc=1 >>non-zero returncode192.168.26.102 | FAILED | rc=1 >>non-zero returncode192.168.26.103 | FAILED | rc=1 >>non-zero returncode192.168.26.101 | FAILED | rc=1 >>non-zero returncode192.168.26.100 | FAILED | rc=1 >>non-zero returncode192.168.26.106 | FAILED | rc=1 >>non-zero returncode
┌──[root@vms100.liruilongs.github.io]
-[~/ansible]
└─$

这里需要注意如果清除所有的镜像缓存,那么需要把
images
下的数组 写成 "".

┌──[root@vms100.liruilongs.github.io]-[~/ansible]
└─$kubectl edit imagecaches imagecache1
-n kube-fledged
imagecache.kubefledged.io
/imagecache1 edited
┌──[root@vms100.liruilongs.github.io]
-[~/ansible]
└─$ansible all
-m shell -a "docker images | grep liruilong/jdk1.8_191" -i host.yaml192.168.26.102 | FAILED | rc=1 >>non-zero returncode192.168.26.101 | FAILED | rc=1 >>non-zero returncode192.168.26.100 | FAILED | rc=1 >>non-zero returncode192.168.26.105 | FAILED | rc=1 >>non-zero returncode192.168.26.103 | FAILED | rc=1 >>non-zero returncode192.168.26.106 | FAILED | rc=1 >>non-zero returncode
┌──[root@vms100.liruilongs.github.io]
-[~/ansible]
└─$kubectl
get imagecaches.kubefledged.io -n kube-fledged imagecache1 -o json
{
"apiVersion": "kubefledged.io/v1alpha2","kind": "ImageCache","metadata": {"creationTimestamp": "2024-03-01T15:08:42Z","generation": 98,"labels": {"app": "kubefledged","kubefledged": "imagecache"},"name": "imagecache1","namespace": "kube-fledged","resourceVersion": "20176849","uid": "3a680a57-d8ab-444f-b9c9-4382459c5c72"},"spec": {"cacheSpec": [
{
"images": [""]
},
{
"images": ["liruilong/hikvision-sdk-config-ftp:latest"],"nodeSelector": {"kubernetes.io/hostname": "vms105.liruilongs.github.io"}
}
]
},
"status": {"completionTime": "2024-03-02T01:52:16Z","message": "All cached images succesfully deleted from respective nodes","reason": "ImageCacheUpdate","startTime": "2024-03-02T01:51:47Z","status": "Succeeded"}
}
┌──[root@vms100.liruilongs.github.io]
-[~/ansible]
└─$

如果通过下面的方式删除,直接注释调对应的标签

┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$cat kubefledged
-imagecache.yaml---apiVersion: kubefledged.io/v1alpha2
kind: ImageCache
metadata:
# Name of the image cache. A cluster can have multiple image cache objects
name: imagecache1
namespace: kube-fledged
# The kubernetes
namespace to be used for this image cache. You can choose a different namepace asper your preference
labels:
app: kubefledged
kubefledged: imagecache
spec:
# The
"cacheSpec" field allows a user to define a list of images and onto which worker nodes those images should be cached (i.e. pre-pulled).
cacheSpec:
# Specifies a list of images (nginx:
1.23.1) with no node selector, hence these images will be cached in all the nodes inthe cluster
#
-images:
#
- liruilong/my-busybox:latest
# Specifies a list of images (cassandra:v7 and etcd:
3.5.4-0) with a node selector, hence these images will be cached only on the nodes selected by the node selector-images:- liruilong/hikvision-sdk-config-ftp:latest
nodeSelector:
kubernetes.io
/hostname: vms105.liruilongs.github.io
# Specifies a list of image pull secrets to pull images
from privaterepositories into the cache
#imagePullSecrets:
#
-name: myregistrykey
┌──[root@vms100.liruilongs.github.io]
-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$

那么会报下面的错

┌──[root@vms100.liruilongs.github.io]-[~/ansible/kube-fledged/kube-fledged/deploy]
└─$kubectl edit imagecaches imagecache1
-n kube-fledged
error: imagecaches.kubefledged.io
"imagecache1" could not be patched: admission webhook "validate-image-cache.kubefledged.io" denied the request: Mismatch inno. of image lists
You can run `kubectl replace
-f /tmp/kubectl-edit-4113815075.yaml` to try this update again.

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知,如果你认可它不要吝啬星星哦 :)

https://github.com/senthilrch/kube-fledged

点击关注,第一时间了解华为云新鲜技术~

1、背景

在日常测试过程中或者研发开发过程中,目前接口暂时没有开发完成,测试人员又要提前介入接口测试中,测试人员不仅仅只是简单的编写测试用例,也可以通过一些mock的方法进行来提前根据接口测试的情况进行模拟返回接口的信息,进行模拟接口各种场景的异常。

mock是指模拟,也就是模拟接口返回的信息,用已有的信息替换它需要返回的信息,从实现对所依赖的模块的测试。

一般有两种场景:

  • 前端对后端接口的 mock,
  • 后端服务之间的测试中涉及的mock,常常发生在单元测试的时候。

前端mock可以通过一些工具来完成:

  • 使用抓包工具Fiddler,Charles 来实现,通过修改代理返回的数据,实现多种场景的测试。
  • 使用一些API管理工具来模拟,比如yapi,Easy Mock 等
  • 当然有编码能力的,也可以使用node.js,python的fastAPI来模拟

后端的 Mock 则是从接口的角度,如果一个接口A返回的数据需要依赖于另一个接口B,当敏捷开发中B接口还未开发完全时候这里会需要用到 Mock。

对于测试人员,对接口测试的时候,部分接口尚未开发完成,在约定了接口定义之后,也可以使用 Mock 来模拟。

今天给大家介绍一款Python Mock工具:
requests-mock

2、工具介绍

requests-mock
是一个用于模拟HTTP请求的Python库,它可以帮助开发人员在测试和开发过程中模拟各种HTTP请求和响应。使用requests-mock,可以用来模拟接口的各种场景,就像真正的服务器一样。

特点:

  1. 灵活性:requests-mock允许开发人员根据需要灵活地定义虚拟的HTTP响应,包括状态码、头部信息、响应体等。
  2. 易用性:requests-mock的API设计简单易用,开发人员可以很容易地集成到他们的测试和开发工作流程中。
  3. 可扩展性:requests-mock支持自定义的响应生成器和请求匹配器,可以满足各种复杂的测试需求。

适用场景:

    1. 单元测试:开发人员可以使用requests-mock模拟HTTP请求和响应,以便在单元测试中测试他们的代码。
    1. 集成测试:在进行集成测试时,requests-mock可以帮助开发人员模拟外部服务的行为,以验证系统的整体功能。
    1. 开发过程中的快速原型验证:在开发过程中,开发人员可以使用requests-mock快速验证他们的代码对于不同的HTTP响应的处理情况。

通过Mock能够帮助我们模拟系统各种行为,包括网络请求、文件读写、数据库操作、系统时间等等。这能够在测试代码时降低对外部依赖的需求,从而提高代码的可测试性。

3、安装

安装必要的模块:requests, requests_mock。

pip install requests
pip install requests_mock

4、使用示例

示例一:使用Mock模拟GET、Post请求

以下是一个使用requests-mock模拟GET请求和POST请求的示例:

import requests
import requests_mock

# 模拟GET请求
with requests_mock.Mocker() as m:
    # 模拟post请求内容,返回的json格式,返回码为200
    m.get('http://example.com/api/data', json={"name":"测试开发技术"}, status_code=200)
    
    response = requests.get('http://example.com/api/data')
    print(response.json())  

# 模拟POST请求
with requests_mock.Mocker() as m:

      # 模拟post请求内容,返回的json格式,返回码为200
    m.post('http://example.com/api/submit',json={"name":"测试开发技术"}, status_code=201)
    
    response = requests.post('http://example.com/api/submit', data={'key': 'value'})
    print(response.json())  

在上面的示例中,我们使用requests-mock模拟了一个GET请求和一个POST请求。在每个模拟的上下文中,我们使用requests_mock.Mocker()创建了一个模拟器,并使用m.get()和m.post()分别定义了GET请求和POST请求的模拟响应。然后,我们使用requests库发送了实际的GET和POST请求,并打印了模拟的响应内容。

示例二:requests-mock在测试脚本中的用法

import requests
import requests_mock

def get_data():
    response = requests.get('http://example.com/api')
    return response.json()
    
def test_get_data():
    adapter = requests_mock.Adapter()
    mock_response = {"status": "ok", "datas": [{"name": "狂师", "description": "公众号:测试开发技术"}]}
    adapter.register_uri('GET', 'http://example.com/api', json=mock_response)

    with requests.Session() as session:
        session.mount('http://', adapter)
        data = get_data()
        assert data["status"] == "ok"
        assert len(news_data["datas"]) == 1
        assert news_data["datas"][0]["name"] == "狂师"    

示例三:requests-mock模拟请求错误异常
正常请求接口的时候,都会出现接口异常情况,比如超时哈,或者请求服务器异常等操作,接下来小编通过requests-mock进行模拟服务器异常的情况。

import requests
import requests_mock
from requests.exceptions import ConnectionError, Timeout

def test_exception():
    with requests_mock.Mocker() as m:
        # 模拟请求超时处理
        m.get('http://example.com/api',exc=Timeout)
        # 通过pytest.raises进行捕捉异常,如果存在异常,则判断为pass
        with pytest.raises(Timeout):
            requests.get('http://example.com/api',timeout=3)
            # 模拟服务器错误
            m.get('http://example.com/api', exc=ConnectionError)
           # 发送请求并断言是否抛出了预期的异常
        with pytest.raises(ConnectionError):
            requests.get('http://example.com/api')