【学习client-go源码,了解管理kubernetes集群的奥秘】—— 连接集群

kubernetes源码学习之client-go overview client-go 是kubernetes中广义的客户端基础库,在Kubernetes各个组件中或多或少都有使用其功能。 Kubernetes API对象 ​ 在 Kubernetes 集群中,Kubernetes 对象是我们持久化的实体,就是最终存入 etcd 中的数据,集群中通过这些实体来表示整个集群的状态。 API 版本 ​ 为了可扩展性,Kubernetes 在不同的 API 路径(比如/api/v1 或者 /apis/batch)下面支持了多个 API 版本,不同的 API 版 阅读详情

学习源码最头疼的是找到入口开始学习,刚开始学习client-go源码的时候,感觉无从下手,后面参考了很多博客,慢慢摸索,从连接k8s集群开始,逐步展开,就可以轻松读懂client-go源码。当然,在学习源码的过程中,也要学习源码的编码风格,学习源码的编程思路,对自己的编程能力也有很大的提高,开始第一篇文章,找到client-go的入口,连接k8s集群。

一、获取源码

1、第一种,在github上面获取如图,直接搜索client-go就可以找到,然后将源码clone到本地就可以了。

在Github中下载client-go

2、第二种执行下面的命令即可。
go  get  k8s.io/client-go

二、探秘

管理k8s的第一步先要连接集群,因此我们先找到client-go源码的入口。 在Github官网上,我们可以找到官方给的example程序。

1、在README中我们可以看到目录中有一个How to use it,点击链接。

How to use it

2、这里有两种选择,第一种是你的程序运行在一个pod中,另一种是你的程序运行在集群外,这里我选择第二种。

example
点击后可以看到下面的界面,里面有一个main.go,这就是我们要找到的入口了。
main.go
点击main.go就可以看到一个示例程序。

package main

import (
	"context"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"time"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
)

func main() {
	var kubeconfig *string
	if home := homeDir(); home != "" {
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	} else {
		kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
	}
	flag.Parse()

	// use the current context in kubeconfig
	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
	if err != nil {
		panic(err.Error())
	}

	// create the clientset
	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		panic(err.Error())
	}
	for {
		pods, err := clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
		if err != nil {
			panic(err.Error())
		}
		fmt.Printf("There are %d pods in the cluster\n", len(pods.Items))

		// Examples for error handling:
		// - Use helper functions like e.g. errors.IsNotFound()
		// - And/or cast to StatusError and use its properties like e.g. ErrStatus.Message
		namespace := "default"
		pod := "example-xxxxx"
		_, err = clientset.CoreV1().Pods(namespace).Get(context.TODO(), pod, metav1.GetOptions{})
		if errors.IsNotFound(err) {
			fmt.Printf("Pod %s in namespace %s not found\n", pod, namespace)
		} else if statusError, isStatus := err.(*errors.StatusError); isStatus {
			fmt.Printf("Error getting pod %s in namespace %s: %v\n",
				pod, namespace, statusError.ErrStatus.Message)
		} else if err != nil {
			panic(err.Error())
		} else {
			fmt.Printf("Found pod %s in namespace %s\n", pod, namespace)
		}

		time.Sleep(10 * time.Second)
	}
}

func homeDir() string {
	if h := os.Getenv("HOME"); h != "" {
		return h
	}
	return os.Getenv("USERPROFILE") // windows
}
3、分析代码,找到入口
在示例代码中有两个函数我们先从main函数开始分析。

(1) main函数中先定义了一个字符串指针变量kubeconfig,接着是调用homeDir函数获取当前家目录,并判断家目录是否为空,如果为空则kubeconfig也为空,不为空则kubeconfig为k8s配置文件目录。从这里看出,client-go应该是通过config文件连接k8s集群的。

(2)接着调用了BuildConfigFromFlags("", *kubeconfig)函数。我们看一下BuildConfigFromFlags函数做了什么。

// BuildConfigFromFlags is a helper function that builds configs from a master
// url or a kubeconfig filepath. These are passed in as command line flags for cluster
// components. Warnings should reflect this usage. If neither masterUrl or kubeconfigPath
// are passed in we fallback to inClusterConfig. If inClusterConfig fails, we fallback
// to the default config.
func BuildConfigFromFlags(masterUrl, kubeconfigPath string) (*restclient.Config, error) {
	if kubeconfigPath == "" && masterUrl == "" {
		klog.Warningf("Neither --kubeconfig nor --master was specified.  Using the inClusterConfig.  This might not work.")
		kubeconfig, err := restclient.InClusterConfig()
		if err == nil {
			return kubeconfig, nil
		}
		klog.Warning("error creating inClusterConfig, falling back to default config: ", err)
	}
	return NewNonInteractiveDeferredLoadingClientConfig(
		&ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
		&ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}}).ClientConfig()
}

从函数的注释可以看出他的主要功能是根据config文件或者masterUrl构建一个Config结构体,该结构体包含了,集群的主要配置信息,原来除了通过配置文件的方式连接k8s集群外,还可以通过masterurl连接集群,官方比较推荐的是通过config文件连接集群。感兴趣的可以看一下restclient.Config,这里不做赘述。

(3)接着调用kubernetes.NewForConfig(config),通过config结构体与k8s集群建立连接。
(4)最后是一个每十秒采集一次pod信息的for循环,在例子中有两个需要注意的点。

第一个是获取集群中的所有pod:

pods, err := clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
if err != nil {
	panic(err.Error())
}
fmt.Printf("There are %d pods in the cluster\n", len(pods.Items))

从上面的第一行代码就可以看出,这行代码的意思是列出集群中的pod,List方法有两个参数,第一个是context上下文,这个可以使用context.TODO生成一个空的上下文,第二个参数,相当于k8s中的Selector,在metav1.ListOptions中你可以进行定义。另外Pods方法中有一个参数是用来筛选NameSpace的,这个Pods方法调用的是newPods:

// newPods returns a Pods
func newPods(c *CoreV1Client, namespace string) *pods {
	return &pods{
		client: c.RESTClient(),
		ns:     namespace,
	}
}

可以看到nowPods返回的是一个pods类型的指针,pods中有两个参数一个是命名空间,一个是连接k8s集群的指针client。
接下来我们看List方法的源码:

// List takes label and field selectors, and returns the list of Pods that match those selectors.
func (c *pods) List(ctx context.Context, opts metav1.ListOptions) (result *v1.PodList, err error) {
	var timeout time.Duration
	if opts.TimeoutSeconds != nil {
		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
	}
	result = &v1.PodList{}
	err = c.client.Get().
		Namespace(c.ns).
		Resource("pods").
		VersionedParams(&opts, scheme.ParameterCodec).
		Timeout(timeout).
		Do(ctx).
		Into(result)
	return
}

可以看到List调用了六个方法,我们来逐一分析:

// Get begins a GET request. Short for c.Verb("GET").
func (c *RESTClient) Get() *Request {
	return c.Verb("GET")
}

Get方法调用Verb方法返回一个Request对象,我们看一下Verb方法:

func (c *RESTClient) Verb(verb string) *Request {
	return NewRequest(c).Verb(verb)
}
// NewRequest creates a new request helper object for accessing runtime.Objects on a server.
func NewRequest(c *RESTClient) *Request {
	var backoff BackoffManager
	if c.createBackoffMgr != nil {
		backoff = c.createBackoffMgr()
	}
	if backoff == nil {
		backoff = noBackoff
	}

	var pathPrefix string
	if c.base != nil {
		pathPrefix = path.Join("/", c.base.Path, c.versionedAPIPath)
	} else {
		pathPrefix = path.Join("/", c.versionedAPIPath)
	}

	var timeout time.Duration
	if c.Client != nil {
		timeout = c.Client.Timeout
	}

	r := &Request{
		c:           c,
		rateLimiter: c.rateLimiter,
		backoff:     backoff,
		timeout:     timeout,
		pathPrefix:  pathPrefix,
		maxRetries:  10,
	}

	switch {
	case len(c.content.AcceptContentTypes) > 0:
		r.SetHeader("Accept", c.content.AcceptContentTypes)
	case len(c.content.ContentType) > 0:
		r.SetHeader("Accept", c.content.ContentType+", */*")
	}
	return r
}

我们可以看到Get方法返回的Request对象其实是NewRequest方法构建的连接API接口的请求参数。
接着看Namespace方法:

// Namespace applies the namespace scope to a request (<resource>/[ns/<namespace>/]<name>)
func (r *Request) Namespace(namespace string) *Request {
	if r.err != nil {
		return r
	}
	if r.namespaceSet {
		r.err = fmt.Errorf("namespace already set to %q, cannot change to %q", r.namespace, namespace)
		return r
	}
	if msgs := IsValidPathSegmentName(namespace); len(msgs) != 0 {
		r.err = fmt.Errorf("invalid namespace %q: %v", namespace, msgs)
		return r
	}
	r.namespaceSet = true
	r.namespace = namespace
	return r
}

这里其实是编辑API请求中的命名空间参数,因此我们可以猜出Resource、VersionParams、Timeout、Do都是用来设置请求参数的,Into则返回请求结果。

第二个需要注意的点是,在源码的pod.go文件中有一个PodInterface接口,在这个接口中定义了pod进行增删改查的方法,也是我们需要关注的点,通过这些方法,我们就可以轻松的管理k8s集群中的pod了。

// PodInterface has methods to work with Pod resources.
type PodInterface interface {
	Create(ctx context.Context, pod *v1.Pod, opts metav1.CreateOptions) (*v1.Pod, error)
	Update(ctx context.Context, pod *v1.Pod, opts metav1.UpdateOptions) (*v1.Pod, error)
	UpdateStatus(ctx context.Context, pod *v1.Pod, opts metav1.UpdateOptions) (*v1.Pod, error)
	Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error
	DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error
	Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Pod, error)
	List(ctx context.Context, opts metav1.ListOptions) (*v1.PodList, error)
	Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error)
	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.Pod, err error)
	GetEphemeralContainers(ctx context.Context, podName string, options metav1.GetOptions) (*v1.EphemeralContainers, error)
	UpdateEphemeralContainers(ctx context.Context, podName string, ephemeralContainers *v1.EphemeralContainers, opts metav1.UpdateOptions) (*v1.EphemeralContainers, error)

	PodExpansion
}

从eample中我们可以看到与k8s建立连接的步骤如下:

初始化
config文件
生成restclient.Config
根据配置生成Clientset
管理集群pod

在后面的博客中我会和大家一起学习如何使用client-go管理k8s集群的pod和node,希对你有所帮助。

go基于kubernetes的二次开发(一) 一些概念: 1.SDK 就是 Software Development Kit 的缩写,中文意思就是【软件开发工具包】 2.client-go是kubernetes官方提供的go语言的客户端库,go应用使用该库可以访问kubernetes的API Server,这样我们就能通过编程来对kubernetes资源进行增删改查操作 3.“API( 应用程序编程接口):一套明确定义的各种软件组件之间的通信方法。 (你的开发工具比如goland要配置好GOROOT和GOPATH) 下载对应版本 (建议,逻辑理解更清 阅读详情

相关推荐

chmod: changing permissions of `/u01/app/oracle/product/11.2.0/dbhome_1/bin/extjobO‘: Operation not

oracle 11.2.0.4安装psu 29141056,日志末尾抛出报错 Patching component oracle.ctx, 11.2.0.4.0... Applying sub-patch '29141056' to OH '/u01/app/oracle/product/11.2.0/dbhome_1' Patching component oracle.rdbms, 11.2.0.4.0... Patching component oracle.rdbms.rman, 11.2.0.

m0_37625564的博客 1833

k8s编程operator——(1) client-go基础部分

client-go是一个golang的client,我们可以通过client-go与K8S apiServer进行交互,对k8s集群中资源对象,包括内置资源(例如:Pod、Deployment、Service等)和CRD进行增删改查等操作。client-go地址目录结构:该包包含了可以访问kubernetes集群的api,通过这些API可以与apiServer进行通信,对集群的资源对象进行增删改查。

Peerless__的博客 5871

【飞腾平台安装windows arm 11系统】

本文提供国内的百度网盘链接供下载使用:链接: https://pan.baidu.com/s/1_cftVdoX40k0M6zVTmFq8Q?3)快速迁移虚拟机技巧:如果是迁移已经安装好的虚拟机的使用场景,除了导出镜像外,也可直接复制安装好虚拟机的整个文件夹到指定的存储目录,选定好其中的任意qcow2文件后即可快速启动。可在飞腾软件平台点击网盘链接获取对应系统的deb包(麒麟和统信)以及phyvirt·app,下载地址:https://www.phytium.com.cn/developer/

firefly_cv的博客 8935

client-go discovery包源码解析

discovery包主要用来发现服务器支持的API组、版本和资源的方法,及服务端支持的api。

任我行的博客 1102

MavLink协议入门

MAVLink是一个非常轻量化的开源的消息协议,用于无人机之间的通信以及无人机机载设备之间的通信。

ffffffeiyu的博客 2474

TouchSocket TcpService:构建高性能Tcp服务的终极利器

/ 接收到的数据data");// 回复客户端await ((ITcpSessionClient)client).SendAsync("服务端已收到消息!");// 继续执行后续插件TouchSocket的TCPService以极简的API、强大的扩展性和卓越的性能,重新定义了.NET TCP服务的开发体验。无论是物联网平台还是金融级实时通信系统,TCPService均能提供可靠的基础设施支持。立即探索示例项目,开启你的高性能服务开发之旅!扩展阅读如何实现WebSocket服务?

若汝棋茗 的博客 1461

k8s之client-go和ctrl的各种k8s client

k8s之client-go和ctrl的各种k8s client

fourierr的博客 920

一文速学数模-K-means聚类算法实战:信用卡用户画像聚类分析

该项目算是非常经典的金融业务用户画像的基础分析了,主要根据用户信用卡使用行为数据进行分析,根据收集到的不同字段信息,对每个用户划分类别。这里需要说明一下的是,聚类模型只是将具有相似行为的大部分用户聚集到一个类别里面,这点并不会考虑到每个字段的含义,也就是分成的类别并不是用户价值等级,此类别仅仅是这个类别大体相同的信用卡用户行为对象,并不能给每个用户价值打上标签,那是评价模型做的事,这里要注意不要搞混淆了,用户价值评价是评价模型决定的,而画像是聚类模型分析的。

master_hunter的博客 1万+

【k8s admission 学习】连接 k8s 集群 InClusterConfig 和 ServiceAccount

ServiceAccount 挂载到 pod 后,一般会在/var/run/secrets/kubernetes.io/serviceaccount路径下(kubectl describe pod podname 会看到具体的挂载路径)实际上k8s中也有一个默认的serviceAccount:default,同样挂载在pod中的上述路径下,只是这个默认的serviceAccount权限很小,所以才会需要创建自定义的serviceAccount。如上:我们要操作pod资源,就要创建如下资源。

叮当猫的喵i 1714

ZW3D 二次开发-制作插件

1.Windows 7 SP1 或 Windows 10 或 Windows 11(推荐 Windows 11)2.Visual studio 2017-2022 (或 C/C++的任何其他集成开发环境)(推荐 Visual studio 2022)3.ZW3D 2023 或以上版本(建议使用与 ZW3D 一致的 API 的版本)4.Qt5.9.7(for Windows)博主学习使用环境的是:Windows 11 Version 24H2 + Visual studio 2022 + Qt5.9.7(fo

fhqiwmc的博客 811

拼多多小球直径轨迹重合验证码识别

本文分析了拼多多新型验证码的实现原理与破解思路。该验证码采用Canvas绘制小球弹跳轨迹验证,主要难点在于参数还原和轨迹计算。作者通过逆向工程提取了包括弹跳段数、振幅、旋转参数等关键配置(存储在config.json),并利用Python成功模拟了验证过程。核心算法将滑块值映射为0-1区间,通过三次变换计算小球位置,最终采用斜率截距最小值比对完成验证。整个过程分为静态初始化、核心计算和动态渲染三个阶段,其中干扰元素不影响核心验证逻辑。该研究仅用于技术学习,强调严禁任何违法行为。

码王吴彦祖的博客 1331

k8s笔记——Client-go 4种客户端与Kubernetes API Server 交互

Client-Go 共提供了 4 种与 Kubernetes APIServer 交互的客户端。分别是 RESTClient、DiscoveryClient、ClientSet、DynamicClient。

mirana的专栏 1469

Python爬虫实战:携程旅游攻略爬取全流程解析(动态接口+反爬破解+异步爬取)

本文介绍了携程旅游攻略的爬取全流程,涵盖静态和动态页面抓取、异步请求、高级反爬策略以及数据存储和简单分析。增加用户评论和评分抓取结合NLP分析攻略文本情感与关键词建立旅游推荐系统自动化代理IP池和验证码识别模块。

2201_76125261的博客 2039

使用client-go实现对K8S集群资源得CRUD操作及源码分析

1、client-go 介绍 client-go 是一种能够与 Kubernetes 集群通信的客户端,通过它可以对 Kubernetes 集群中各资源类型进行 CRUD 操作,它有三大 client 类,分别为:Clientset、DynamicClient、RESTClient。通过它,我们可以很方便的对 Kubernetes 集群 API 进行自定义开发,来满足个性化需求。 2、client...

fy_long的博客 6206

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

[k8s源码]1.client-go集群外部署

client-go是由k8s发布且维护的专门用于开发者和kubernetes交互的客户端库。它支持对k8s资源的CRUD操作(create、read、update、delete),事件监听和处理,访问kubernetes集群的上下文和配置。client go是独立于kubernetes集群之外,但是又和集群交互的客户端机器。在这台机器上安装go环境以及和kubernetes建立连接。

weixin_45396500的博客 1608

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

使用client-go自定义开发Kubernetes

1. 安装client-go client-go 安装很简单,前提是本机已经安装并配置好了 Go 环境,安装之前,我们需要先查看下其版本针对 k8s 版本 兼容性列表,针对自己本机安装的 k8s 版本选择对应的 client-go 版本,当然也可以默认选择最新版本,来兼容所有。 client-go 安装方式有多种,比如 go get、Godep、Glide 方式。如果我们本地没有安装 Gode...

kubernetes中文社区 6505

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

client-go源码结构及客户端对象

(1)client-go是一个调用kubernetes集群资源对象API的客户端,即通过client-go实现对kubernetes集群中资源对象(包括deployment、service、ingress、replicaSet、pod、namespace、node等)的增删改查等操作。(2)client-go支持RESTClientClientSet四种客户端与Kubernetes Api Server进行交互。RESTClient是最基础的客户端。

orz_include的博客 1211

python开发k8s管理平台_【K8S】client-go、python-k8sclient开发K8S

0x01 client-go1、简介Client-go是kubernetes官方发布的调用K8S API的golang语言包,可以用来开发K8S的管理服务、监控服务,配合前端展示,就可以开发出一款定制化的、可视化的管理或监控工具。目前最新版本为7.0,对应K8S的版本为1.10,访问链接:https://github.com/kubernetes/client-go其中client-go与K8S版...

weixin_39962125的博客 678

K8s集群证书续签100年有效期

注意:若出现 ./hack/run-in-gopath.sh:行34: _output/bin/prerelease-lifecycle-gen: 权限不够 问题时,需要添加权限后再编译源码文件。上传01管理节点中编译后的kubeadm二进制文件到02管理节点和03管理节点,02管理节点与03管理节点证书有效期调整,操作步骤一致,以下为02管理节点操作过程。替换新编译的kubeadm文件覆盖旧的kubeadm文件。替换新编译的kubeadm文件覆盖旧的kubeadm文件。备份原有的kubeadm文件。

m0_50978391的博客 1141

client-go的使用及源码分析

本文个人博客地址:http://www.huweihuang.com/article/source-analysis/client-go-source-analysis/ 1. client-go简介 1.1 client-go说明 ​ client-go是一个调用kubernetes集群资源对象API的客户端,即通过client-go实现对kubernetes集群中资源对象(包

胡伟煌的博客 2万+

4. client-go 编程式交互

Kubernetes 系统使用 client-go 作为 Go 语言的官方编程式交互客户端库,提供对 Kubernetes API Server 服务的交互访问。Kubernetes 的源码中已经集成了 client-go 的源码,无须单独下载。client-go 源码路径为 。开发者经常使用 client-go 基于 Kubernetes 做二次开发,所以 client-go 是开发者应熟练掌握的必会技能。client-go 的代码库已经集成到 Kubernetes 源码中了,无须考虑版本兼容性问题,源码

qq_32468785的博客 1292

使用 client-go 对 Kubernetes 进行自定义开发及源码分析

client-go 是一种能够与 Kubernetes 集群通信的客户端,通过它可以对 Kubernetes 集群中各资源类型进行 CRUD 操作,它有三大 client 类,分别为:Clientset、DynamicClient、RESTClient。通过它,我们可以很方便的对 Kubernetes 集群 API 进行自定义开发,来满足个性化需求。本文介绍如何使用 client-go 对 k8s 集群进行自定义开发及源码分析。

哎_小羊的博客 6429

Client-go

想要读懂kubernetes 源码,首先要理解client-go, 因为client-go 是 Kubernetes 官方的 Go 语言客户端库,被 Kubernetes 中的众多组件和功能模块使用。其中包括 kube-apiserver、kube-controller-manager、kube-scheduler 等核心组件,以及一些扩展功能如 kube-proxy、kubelet 等。

liquanfan的博客 1924

sigs.k8s.io controller-runtime系列之四 client分析

简介 之前介绍过sigs.k8s.io controller-runtime系列之三 controller分析sigs.k8s.io controller-runtime-controller 。 本文主要介绍pkg/client的源码分析。 目录结构 interfaces.go Patch接口 是可以应用于Kubernetes对象的补丁 type Patch interface { // Patch的类型(字符串形式 "application/json-patch+json"/"applic

qq_36407557的博客 4930

k8s源码阅读之client-go第一二章

client-go 客户端对象 1、RestClient (基础) 2、ClientSet 基于RestClient封装Resource和Version管理方法仅能处理k8s内置资源(内置资源包括:https://www.cnblogs.com/hongdada/p/11597835.html)----源码位置(kubernetes\vendor\k8s.io\client-go\kubernetes) 3、DynamicClient 既可以访问k8s内置资源又可以访问CRD(自定义资源) 4、Disco

weixin_39637585的博客 218
上一篇: 使用minikube部署单机版k8s
下一篇: Jenkins学习篇之在Jenkins上配置kubernetes
random_w
博客等级 码龄9年 130粉丝 168原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值