kubectl源码分析之apply

kubectl滚动更新资源 rollout Manage the rollout of a resource. Valid resource types include: * deployments * daemonsets * statefulsets Examples: # Rollback to the previous deployment kubectl rollout undo deployment/abc # Check the rollout status of a da 阅读详情

 欢迎关注我的公众号:

 目前刚开始写一个月,一共写了18篇原创文章,文章目录如下:

istio多集群探秘,部署了50次多集群后我得出的结论

istio多集群链路追踪,附实操视频

istio防故障利器,你知道几个,istio新手不要读,太难!

istio业务权限控制,原来可以这么玩

istio实现非侵入压缩,微服务之间如何实现压缩

不懂envoyfilter也敢说精通istio系列-http-rbac-不要只会用AuthorizationPolicy配置权限

不懂envoyfilter也敢说精通istio系列-02-http-corsFilter-不要只会vs

不懂envoyfilter也敢说精通istio系列-03-http-csrf filter-再也不用再代码里写csrf逻辑了

不懂envoyfilter也敢说精通istio系列http-jwt_authn-不要只会RequestAuthorization

不懂envoyfilter也敢说精通istio系列-05-fault-filter-故障注入不止是vs

不懂envoyfilter也敢说精通istio系列-06-http-match-配置路由不只是vs

不懂envoyfilter也敢说精通istio系列-07-负载均衡配置不止是dr

不懂envoyfilter也敢说精通istio系列-08-连接池和断路器

不懂envoyfilter也敢说精通istio系列-09-http-route filter

不懂envoyfilter也敢说精通istio系列-network filter-redis proxy

不懂envoyfilter也敢说精通istio系列-network filter-HttpConnectionManager

不懂envoyfilter也敢说精通istio系列-ratelimit-istio ratelimit完全手册

 

————————————————

type ApplyOptions struct {//apply结构体
	RecordFlags *genericclioptions.RecordFlags
	Recorder    genericclioptions.Recorder

	PrintFlags *genericclioptions.PrintFlags
	ToPrinter  func(string) (printers.ResourcePrinter, error)

	DeleteFlags   *delete.DeleteFlags
	DeleteOptions *delete.DeleteOptions

	ServerSideApply bool
	ForceConflicts  bool
	FieldManager    string
	Selector        string
	DryRun          bool
	ServerDryRun    bool
	Prune           bool
	PruneResources  []pruneResource
	cmdBaseName     string
	All             bool
	Overwrite       bool
	OpenAPIPatch    bool
	PruneWhitelist  []string

	Validator       validation.Schema
	Builder         *resource.Builder
	Mapper          meta.RESTMapper
	DynamicClient   dynamic.Interface
	DiscoveryClient discovery.DiscoveryInterface
	OpenAPISchema   openapi.Resources

	Namespace        string
	EnforceNamespace bool

	genericclioptions.IOStreams
}
func NewApplyOptions(ioStreams genericclioptions.IOStreams) *ApplyOptions {
	return &ApplyOptions{//初始化apply结构体
		RecordFlags: genericclioptions.NewRecordFlags(),
		DeleteFlags: delete.NewDeleteFlags("that contains the configuration to apply"),
		PrintFlags:  genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),

		Overwrite:    true,
		OpenAPIPatch: true,

		Recorder: genericclioptions.NoopRecorder{},

		IOStreams: ioStreams,
	}
}
//创建apply命令
func NewCmdApply(baseName string, f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
	o := NewApplyOptions(ioStreams)//初始化结构体

	// Store baseName for use in printing warnings / messages involving the base command name.
	// This is useful for downstream command that wrap this one.
	o.cmdBaseName = baseName//基命令名称

	cmd := &cobra.Command{//创建cobra命令
		Use:                   "apply (-f FILENAME | -k DIRECTORY)",
		DisableFlagsInUseLine: true,
		Short:                 i18n.T("Apply a configuration to a resource by filename or stdin"),
		Long:                  applyLong,
		Example:               applyExample,
		Run: func(cmd *cobra.Command, args []string) {
			cmdutil.CheckErr(o.Complete(f, cmd))//准备
			cmdutil.CheckErr(validateArgs(cmd, args))//校验
			cmdutil.CheckErr(validatePruneAll(o.Prune, o.All, o.Selector))//校验
			cmdutil.CheckErr(o.Run())//运行
		},
	}

	// bind flag structs
	o.DeleteFlags.AddFlags(cmd)//删除选项
	o.RecordFlags.AddFlags(cmd)//record选项
	o.PrintFlags.AddFlags(cmd)//打印选项

	cmd.Flags().BoolVar(&o.Overwrite, "overwrite", o.Overwrite, "Automatically resolve conflicts between the modified and live configuration by using values from the modified configuration")//客户端patch时是否覆盖选项
	cmd.Flags().BoolVar(&o.Prune, "prune", o.Prune, "Automatically delete resource objects, including the uninitialized ones, that do not appear in the configs and are created by either apply or create --save-config. Should be used with either -l or --all.")//是否修剪选项
	cmdutil.AddValidateFlags(cmd)//校验选项
	cmd.Flags().StringVarP(&o.Selector, "selector", "l", o.Selector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)")//selector选项
	cmd.Flags().BoolVar(&o.All, "all", o.All, "Select all resources in the namespace of the specified resource types.")//all选项
	cmd.Flags().StringArrayVar(&o.PruneWhitelist, "prune-whitelist", o.PruneWhitelist, "Overwrite the default whitelist with <group/version/kind> for --prune")//prune白名单选项
	cmd.Flags().BoolVar(&o.OpenAPIPatch, "openapi-patch", o.OpenAPIPatch, "If true, use openapi to calculate diff when the openapi presents and the resource can be found in the openapi spec. Otherwise, fall back to use baked-in types.")//用openapi比较差异选项
	cmd.Flags().BoolVar(&o.ServerDryRun, "server-dry-run", o.ServerDryRun, "If true, request will be sent to server with dry-run flag, which means the modifications won't be persisted. This is an alpha feature and flag.")//服务器干跑选项
	cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without sending it. Warning: --dry-run cannot accurately output the result of merging the local manifest and the server-side data. Use --server-dry-run to get the merged result instead.")//干跑选项
	cmdutil.AddIncludeUninitializedFlag(cmd)
	cmdutil.AddServerSideApplyFlags(cmd)//服务端patch选项

	// apply subcommands
	cmd.AddCommand(NewCmdApplyViewLastApplied(f, ioStreams))//添加view命令
	cmd.AddCommand(NewCmdApplySetLastApplied(f, ioStreams))//添加set命令
	cmd.AddCommand(NewCmdApplyEditLastApplied(f, ioStreams))//添加edit选项

	return cmd
}
//准备
func (o *ApplyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error {
	o.ServerSideApply = cmdutil.GetServerSideApplyFlag(cmd)//获取server-side选项
	o.ForceConflicts = cmdutil.GetForceConflictsFlag(cmd)//获取force-confilict选项
	o.FieldManager = cmdutil.GetFieldManagerFlag(cmd)//获取field-manager选项
	o.DryRun = cmdutil.GetDryRunFlag(cmd)//获取干跑选项

	if o.ForceConflicts && !o.ServerSideApply {//如果指定了force-conflicts则server-side必须指定
		return fmt.Errorf("--force-conflicts only works with --server-side")
	}

	if o.DryRun && o.ServerSideApply {//客户端干跑和server-side不能同时指定
		return fmt.Errorf("--dry-run doesn't work with --server-side (did you mean --server-dry-run instead?)")
	}

	if o.DryRun && o.ServerDryRun {//客户端干跑和服务端干跑不能同时指定
		return fmt.Errorf("--dry-run and --server-dry-run can't be used together")
	}

	// allow for a success message operation to be specified at print time
	o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {//print 选项转printer函数
		o.PrintFlags.NamePrintFlags.Operation = operation
		if o.DryRun {
			o.PrintFlags.Complete("%s (dry run)")
		}
		if o.ServerDryRun {
			o.PrintFlags.Complete("%s (server dry run)")
		}
		return o.PrintFlags.ToPrinter()
	}

	var err error
	o.RecordFlags.Complete(cmd)//准备record
	o.Recorder, err = o.RecordFlags.ToRecorder()//record选项转recorder
	if err != nil {
		return err
	}

	o.DiscoveryClient, err = f.ToDiscoveryClient()//设置discoveryClient
	if err != nil {
		return err
	}

	dynamicClient, err := f.DynamicClient()//设置dynamicClient
	if err != nil {
		return err
	}
	o.DeleteOptions = o.DeleteFlags.ToOptions(dynamicClient, o.IOStreams)//delete选项转deleteOption
	err = o.DeleteOptions.FilenameOptions.RequireFilenameOrKustomize()//文件必须
	if err != nil {
		return err
	}

	o.OpenAPISchema, _ = f.OpenAPISchema()//设置OpenApiSchema
	o.Validator, err = f.Validator(cmdutil.GetFlagBool(cmd, "validate"))//设置validator校验器
	o.Builder = f.NewBuilder()//设置builder
	o.Mapper, err = f.ToRESTMapper()//设置restMapper
	if err != nil {
		return err
	}

	o.DynamicClient, err = f.DynamicClient()//设置dynamicClient
	if err != nil {
		return err
	}

	o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()//设置namespace和enforceNamespace
	if err != nil {
		return err
	}

	return nil
}
//校验参数
func validateArgs(cmd *cobra.Command, args []string) error {
	if len(args) != 0 {//参数必须是0个
		return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args)
	}
	return nil
}

//校验prune参数
func validatePruneAll(prune, all bool, selector string) error {
	if all && len(selector) > 0 {//all和selector不能同时指定
		return fmt.Errorf("cannot set --all and --selector at the same time")
	}
	if prune && !all && selector == "" {//如果指定了prune必须指定all或selector
		return fmt.Errorf("all resources selected for prune without explicitly passing --all. To prune all resources, pass the --all flag. If you did not mean to prune all resources, specify a label selector")
	}
	return nil
}
//运行
func (o *ApplyOptions) Run() error {
	var openapiSchema openapi.Resources
	if o.OpenAPIPatch {//如果指定了用openapi比较patch则设置openapiSchema
		openapiSchema = o.OpenAPISchema
	}

	dryRunVerifier := &DryRunVerifier{//构造干跑校验器
		Finder:        cmdutil.NewCRDFinder(cmdutil.CRDFromDynamic(o.DynamicClient)),
		OpenAPIGetter: o.DiscoveryClient,
	}

	// include the uninitialized objects by default if --prune is true
	// unless explicitly set --include-uninitialized=false
	r := o.Builder.
		Unstructured().
		Schema(o.Validator).
		ContinueOnError().
		NamespaceParam(o.Namespace).DefaultNamespace().
		FilenameParam(o.EnforceNamespace, &o.DeleteOptions.FilenameOptions).
		LabelSelectorParam(o.Selector).
		Flatten().
		Do()//用builder构造result对象
	if err := r.Err(); err != nil {// result有错误返回错误
		return err
	}

	var err error
	if o.Prune {//如果指定了prune,解析prune资源
		o.PruneResources, err = parsePruneResources(o.Mapper, o.PruneWhitelist)
		if err != nil {
			return err
		}
	}

	output := *o.PrintFlags.OutputFormat
	shortOutput := output == "name"//如果--output为name则短输出为true

	visitedUids := sets.NewString()//访问过的uid的string集合
	visitedNamespaces := sets.NewString()//访问过的namespace的string集合

	var objs []runtime.Object//Object切片

	count := 0//count初始值为0
	err = r.Visit(func(info *resource.Info, err error) error {//visit result
		if err != nil {
			return err
		}

		// If server-dry-run is requested but the type doesn't support it, fail right away.
		if o.ServerDryRun {//如果指定了服务端干跑,校验gvk是否支持干跑
			if err := dryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
				return err
			}
		}

		if info.Namespaced() {//如果info是名称空间资源,则把名称空间加到访问过的名称空间集合里
			visitedNamespaces.Insert(info.Namespace)
		}

		if err := o.Recorder.Record(info.Object); err != nil {//判断是否创建change-cause注解
			klog.V(4).Infof("error recording current command: %v", err)
		}

		if o.ServerSideApply {//如果指定了server-side
			// Send the full object to be applied on the server side.
			data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, info.Object)//把object编码为json
			if err != nil {
				return cmdutil.AddSourceToErr("serverside-apply", info.Source, err)
			}

			options := metav1.PatchOptions{//创建patchOption结构体
				Force:        &o.ForceConflicts,
				FieldManager: o.FieldManager,
			}
			if o.ServerDryRun {//如果指定了服务端干跑,则设置option干跑属性
				options.DryRun = []string{metav1.DryRunAll}
			}

			obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(//应用全量patch到服务端
				info.Namespace,
				info.Name,
				types.ApplyPatchType,
				data,
				&options,
			)
			if err != nil {
				if isIncompatibleServerError(err) {
					err = fmt.Errorf("Server-side apply not available on the server: (%v)", err)
				}
				if errors.IsConflict(err) {
					err = fmt.Errorf(`%v

Please review the fields above--they currently have other managers. Here
are the ways you can resolve this warning:
* If you intend to manage all of these fields, please re-run the apply
  command with the `+"`--force-conflicts`"+` flag.
* If you do not intend to manage all of the fields, please edit your
  manifest to remove references to the fields that should keep their
  current managers.
* You may co-own fields by updating your manifest to match the existing
  value; in this case, you'll become the manager if the other manager(s)
  stop managing the field (remove it from their configuration).

See http://k8s.io/docs/reference/using-api/api-concepts/#conflicts`, err)
				}
				return err
			}

			info.Refresh(obj, true)//刷新对象
			metadata, err := meta.Accessor(info.Object)//访问对象元数据
			if err != nil {
				return err
			}

			visitedUids.Insert(string(metadata.GetUID()))//获取对象uid,插入到访问过的uid集合里
			count++// count 加1
			if len(output) > 0 && !shortOutput {//如果指定了output并且不是shortOutput则添加对象到object集合里
				objs = append(objs, info.Object)
				return nil
			}

			printer, err := o.ToPrinter("serverside-applied")// printflag转printer
			if err != nil {
				return err
			}

			return printer.PrintObj(info.Object, o.Out)//打印对象
		}

		// Get the modified configuration of the object. Embed the result
		// as an annotation in the modified configuration, so that it will appear
		// in the patch sent to the server.
		modified, err := util.GetModifiedConfiguration(info.Object, true, unstructured.UnstructuredJSONScheme)//获取修改后的对象配置信息
		if err != nil {
			return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving modified configuration from:\n%s\nfor:", info.String()), info.Source, err)
		}

		// Print object only if output format other than "name" is specified
		printObject := len(output) > 0 && !shortOutput//如果指定了output并且不是shortOutput则打印对象为true

		if err := info.Get(); err != nil {//获取远程info对象
			if !errors.IsNotFound(err) {//如果不是非找到错误则返回错误
				return cmdutil.AddSourceToErr(fmt.Sprintf("retrieving current configuration of:\n%s\nfrom server for:", info.String()), info.Source, err)
			}

			// Create the resource if it doesn't exist
			// First, update the annotation used by kubectl apply
			if err := util.CreateApplyAnnotation(info.Object, unstructured.UnstructuredJSONScheme); err != nil {//判断是否创建last-applied-configuration注解
				return cmdutil.AddSourceToErr("creating", info.Source, err)
			}

			if !o.DryRun {//如果是非干跑
				// Then create the resource and skip the three-way merge
				options := metav1.CreateOptions{}//创建createOption
				if o.ServerDryRun {//如果是服务端干跑
					options.DryRun = []string{metav1.DryRunAll}
				}
				obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object, &options)//创建对象到服务端
				if err != nil {
					return cmdutil.AddSourceToErr("creating", info.Source, err)
				}
				info.Refresh(obj, true)//刷新对象
			}

			metadata, err := meta.Accessor(info.Object)// 访问对象元数据
			if err != nil {
				return err
			}
			visitedUids.Insert(string(metadata.GetUID()))//添加对象uid到访问过的uid列表里

			count++

			if printObject {//如果是打印对象,则把对象加到objs切片里
				objs = append(objs, info.Object)
				return nil
			}

			printer, err := o.ToPrinter("created")//print flag转printer
			if err != nil {
				return err
			}
			return printer.PrintObj(info.Object, o.Out)//打印对象
		}

		metadata, err := meta.Accessor(info.Object)//访问对象元数据
		if err != nil {
			return err
		}
		visitedUids.Insert(string(metadata.GetUID()))//把对象uid加到访问过的uid里面

		if !o.DryRun {//非干跑
			annotationMap := metadata.GetAnnotations()//获取注解
			if _, ok := annotationMap[corev1.LastAppliedConfigAnnotation]; !ok {//如果没有last-applied-configuration注解输出告警
				fmt.Fprintf(o.ErrOut, warningNoLastAppliedConfigAnnotation, o.cmdBaseName)
			}

			helper := resource.NewHelper(info.Client, info.Mapping)//创建helper
			patcher := &Patcher{//创建patcher结构体
				Mapping:       info.Mapping,
				Helper:        helper,
				DynamicClient: o.DynamicClient,
				Overwrite:     o.Overwrite,
				BackOff:       clockwork.NewRealClock(),
				Force:         o.DeleteOptions.ForceDeletion,
				Cascade:       o.DeleteOptions.Cascade,
				Timeout:       o.DeleteOptions.Timeout,
				GracePeriod:   o.DeleteOptions.GracePeriod,
				ServerDryRun:  o.ServerDryRun,
				OpenapiSchema: openapiSchema,
				Retries:       maxPatchRetry,
			}

			patchBytes, patchedObject, err := patcher.Patch(info.Object, modified, info.Source, info.Namespace, info.Name, o.ErrOut)//应用patch到服务端
			if err != nil {
				return cmdutil.AddSourceToErr(fmt.Sprintf("applying patch:\n%s\nto:\n%v\nfor:", patchBytes, info), info.Source, err)
			}

			info.Refresh(patchedObject, true)//刷新对象

			if string(patchBytes) == "{}" && !printObject {//如果patchBytes为空,并且不打印对象
				count++

				printer, err := o.ToPrinter("unchanged")//print flag转printer
				if err != nil {
					return err
				}
				return printer.PrintObj(info.Object, o.Out)// 打印对象
			}
		}
		count++

		if printObject {//如果打印对象,把对象放到objs切片中
			objs = append(objs, info.Object)
			return nil
		}

		printer, err := o.ToPrinter("configured")//print flag转printer
		if err != nil {
			return err
		}
		return printer.PrintObj(info.Object, o.Out)//打印对象
	})
	if err != nil {
		return err
	}

	if count == 0 {//count为0个返回错误
		return fmt.Errorf("no objects passed to apply")
	}

	// print objects
	if len(objs) > 0 {// 如果objs个数大于0个
		printer, err := o.ToPrinter("")//print flag转printer
		if err != nil {
			return err
		}

		objToPrint := objs[0]
		if len(objs) > 1 {//如果objs大于1个
			list := &corev1.List{//创建list对象
				TypeMeta: metav1.TypeMeta{
					Kind:       "List",
					APIVersion: "v1",
				},
				ListMeta: metav1.ListMeta{},
			}
			if err := meta.SetList(list, objs); err != nil {
				return err
			}

			objToPrint = list//打印的对象设为list
		}
		if err := printer.PrintObj(objToPrint, o.Out); err != nil {//打印对象
			return err
		}
	}

	if !o.Prune {//如果没有指定prune,则返回
		return nil
	}

	p := pruner{//构造pruner对象
		mapper:        o.Mapper,
		dynamicClient: o.DynamicClient,

		labelSelector: o.Selector,
		visitedUids:   visitedUids,

		cascade:      o.DeleteOptions.Cascade,
		dryRun:       o.DryRun,
		serverDryRun: o.ServerDryRun,
		gracePeriod:  o.DeleteOptions.GracePeriod,

		toPrinter: o.ToPrinter,

		out: o.Out,
	}

	namespacedRESTMappings, nonNamespacedRESTMappings, err := getRESTMappings(o.Mapper, &(o.PruneResources))//获取namespacedRestMapping和nonNamespacedRestmapping
	if err != nil {
		return fmt.Errorf("error retrieving RESTMappings to prune: %v", err)
	}

	for n := range visitedNamespaces {//遍历访问过的名称空间
		for _, m := range namespacedRESTMappings {//遍历namespacedRESTMappings 
			if err := p.prune(n, m); err != nil {//执行修剪
				return fmt.Errorf("error pruning namespaced object %v: %v", m.GroupVersionKind, err)
			}
		}
	}
	for _, m := range nonNamespacedRESTMappings {//遍历nonNamespacedRESTMappings 
		if err := p.prune(metav1.NamespaceNone, m); err != nil {//执行修剪
			return fmt.Errorf("error pruning nonNamespaced object %v: %v", m.GroupVersionKind, err)
		}
	}

	return nil
}
//解析prune资源
func parsePruneResources(mapper meta.RESTMapper, gvks []string) ([]pruneResource, error) {
	pruneResources := []pruneResource{}
	for _, groupVersionKind := range gvks {//遍历gvks slice
		gvk := strings.Split(groupVersionKind, "/")//用/分割
		if len(gvk) != 3 {//分割结果不为3个报错
			return nil, fmt.Errorf("invalid GroupVersionKind format: %v, please follow <group/version/kind>", groupVersionKind)
		}

		if gvk[0] == "core" {//如果第0个结果为core,则设置为空字符串
			gvk[0] = ""
		}
		mapping, err := mapper.RESTMapping(schema.GroupKind{Group: gvk[0], Kind: gvk[2]}, gvk[1])//根据gvk获取mapping
		if err != nil {
			return pruneResources, err
		}
		var namespaced bool
		namespaceScope := mapping.Scope.Name()//判断是否名称空间资源
		switch namespaceScope {
		case meta.RESTScopeNameNamespace:
			namespaced = true
		case meta.RESTScopeNameRoot:
			namespaced = false
		default:
			return pruneResources, fmt.Errorf("Unknown namespace scope: %q", namespaceScope)
		}

		pruneResources = append(pruneResources, pruneResource{gvk[0], gvk[1], gvk[2], namespaced})//append pruneREsource
	}
	return pruneResources, nil
}
//判断gvk是否支持干跑
func (v *DryRunVerifier) HasSupport(gvk schema.GroupVersionKind) error {
	oapi, err := v.OpenAPIGetter.OpenAPISchema()//获取openapiSchema
	if err != nil {
		return fmt.Errorf("failed to download openapi: %v", err)
	}
	supports, err := openapi.SupportsDryRun(oapi, gvk)//判断是否支持干跑
	if err != nil {
		// We assume that we couldn't find the type, then check for namespace:
		supports, _ = openapi.SupportsDryRun(oapi, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"})//判断是否支持干跑
		// If namespace supports dryRun, then we will support dryRun for CRDs only.
		if supports {
			supports, err = v.Finder.HasCRD(gvk.GroupKind())
			if err != nil {
				return fmt.Errorf("failed to check CRD: %v", err)
			}
		}
	}
	if !supports {//不支持则返回错误
		return fmt.Errorf("%v doesn't support dry-run", gvk)
	}
	return nil
}
//获取修改后的配置
func GetModifiedConfiguration(obj runtime.Object, annotate bool, codec runtime.Encoder) ([]byte, error) {
	// First serialize the object without the annotation to prevent recursion,
	// then add that serialization to it as the annotation and serialize it again.
	var modified []byte

	// Otherwise, use the server side version of the object.
	// Get the current annotations from the object.
	annots, err := metadataAccessor.Annotations(obj)//获取annotations
	if err != nil {
		return nil, err
	}

	if annots == nil {
		annots = map[string]string{}
	}

	original := annots[v1.LastAppliedConfigAnnotation]//保存原始的last-applied-configuation注解
	delete(annots, v1.LastAppliedConfigAnnotation)//删除last-applied-configuration注解
	if err := metadataAccessor.SetAnnotations(obj, annots); err != nil {//设置注解
		return nil, err
	}

	modified, err = runtime.Encode(codec, obj)//编码obj成json
	if err != nil {
		return nil, err
	}

	if annotate {// 如果包含annotations
		annots[v1.LastAppliedConfigAnnotation] = string(modified)//设置last-applied-configuation为修改后的json
		if err := metadataAccessor.SetAnnotations(obj, annots); err != nil {//设置注解
			return nil, err
		}

		modified, err = runtime.Encode(codec, obj)//编码obj为json
		if err != nil {
			return nil, err
		}
	}

	// Restore the object to its original condition.
	annots[v1.LastAppliedConfigAnnotation] = original//还原注解
	if err := metadataAccessor.SetAnnotations(obj, annots); err != nil {//设置注解
		return nil, err
	}

	return modified, nil//返回json
}
//应用增量patch到服务端
func (p *Patcher) Patch(current runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) {
	var getErr error
	patchBytes, patchObject, err := p.patchSimple(current, modified, source, namespace, name, errOut)//应用patch
	if p.Retries == 0 {
		p.Retries = maxPatchRetry//设置重试次数
	}
	for i := 1; i <= p.Retries && errors.IsConflict(err); i++ {//如果错误是冲突错误,并小于重试次数,则循环应用patch到服务端
		if i > triesBeforeBackOff {
			p.BackOff.Sleep(backOffPeriod)
		}
		current, getErr = p.Helper.Get(namespace, name, false)//获取当前对象
		if getErr != nil {
			return nil, nil, getErr
		}
		patchBytes, patchObject, err = p.patchSimple(current, modified, source, namespace, name, errOut)//引用patch到服务端
	}
	if err != nil && (errors.IsConflict(err) || errors.IsInvalid(err)) && p.Force {//如果指定force,并且有错误,错误为冲突或无效,则执行删除后创建
		patchBytes, patchObject, err = p.deleteAndCreate(current, modified, namespace, name)
	}
	return patchBytes, patchObject, err
}
//应用patch到服务端
func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, source, namespace, name string, errOut io.Writer) ([]byte, runtime.Object, error) {
	// Serialize the current configuration of the object from the server.
	current, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)//把当前对象转成json
	if err != nil {
		return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("serializing current configuration from:\n%v\nfor:", obj), source, err)
	}

	// Retrieve the original configuration of the object from the annotation.
	original, err := util.GetOriginalConfiguration(obj)//获取原始对象json
	if err != nil {
		return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("retrieving original configuration from:\n%v\nfor:", obj), source, err)
	}

	var patchType types.PatchType
	var patch []byte
	var lookupPatchMeta strategicpatch.LookupPatchMeta
	var schema oapi.Schema
	createPatchErrFormat := "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor:"

	// Create the versioned struct from the type defined in the restmapping
	// (which is the API version we'll be submitting the patch to)
	versionedObject, err := scheme.Scheme.New(p.Mapping.GroupVersionKind)//获取schema对象
	switch {
	case runtime.IsNotRegisteredError(err):
		// fall back to generic JSON merge patch
		patchType = types.MergePatchType
		preconditions := []mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"),
			mergepatch.RequireKeyUnchanged("kind"), mergepatch.RequireMetadataKeyUnchanged("name")}
		patch, err = jsonmergepatch.CreateThreeWayJSONMergePatch(original, modified, current, preconditions...)
		if err != nil {
			if mergepatch.IsPreconditionFailed(err) {
				return nil, nil, fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed")
			}
			return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
		}
	case err != nil:
		return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf("getting instance of versioned object for %v:", p.Mapping.GroupVersionKind), source, err)
	case err == nil:
		// Compute a three way strategic merge patch to send to server.
		patchType = types.StrategicMergePatchType//设置merge策略

		// Try to use openapi first if the openapi spec is available and can successfully calculate the patch.
		// Otherwise, fall back to baked-in types.
		if p.OpenapiSchema != nil {
			if schema = p.OpenapiSchema.LookupResource(p.Mapping.GroupVersionKind); schema != nil {
				lookupPatchMeta = strategicpatch.PatchMetaFromOpenAPI{Schema: schema}
				if openapiPatch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite); err != nil {//创建patch
					fmt.Fprintf(errOut, "warning: error calculating patch from openapi spec: %v\n", err)
				} else {
					patchType = types.StrategicMergePatchType
					patch = openapiPatch//设置patch
				}
			}
		}

		if patch == nil {
			lookupPatchMeta, err = strategicpatch.NewPatchMetaFromStruct(versionedObject)//获取patch元数据
			if err != nil {
				return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
			}
			patch, err = strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, p.Overwrite)//创建patch
			if err != nil {
				return nil, nil, cmdutil.AddSourceToErr(fmt.Sprintf(createPatchErrFormat, original, modified, current), source, err)
			}
		}
	}

	if string(patch) == "{}" {
		return patch, obj, nil
	}

	if p.ResourceVersion != nil {
		patch, err = addResourceVersion(patch, *p.ResourceVersion)//设置patch资源
		if err != nil {
			return nil, nil, cmdutil.AddSourceToErr("Failed to insert resourceVersion in patch", source, err)
		}
	}

	options := metav1.PatchOptions{}
	if p.ServerDryRun {
		options.DryRun = []string{metav1.DryRunAll}
	}

	patchedObj, err := p.Helper.Patch(namespace, name, patchType, patch, &options)//应用patch到服务端
	return patch, patchedObj, err
}
//删除重建资源
func (p *Patcher) deleteAndCreate(original runtime.Object, modified []byte, namespace, name string) ([]byte, runtime.Object, error) {
	if err := p.delete(namespace, name); err != nil {//删除资源
		return modified, nil, err
	}
	// TODO: use wait
	if err := wait.PollImmediate(1*time.Second, p.Timeout, func() (bool, error) {//等待资源删除完成
		if _, err := p.Helper.Get(namespace, name, false); !errors.IsNotFound(err) {
			return false, err
		}
		return true, nil
	}); err != nil {
		return modified, nil, err
	}
	versionedObject, _, err := unstructured.UnstructuredJSONScheme.Decode(modified, nil, nil)//把modified转成json
	if err != nil {
		return modified, nil, err
	}
	options := metav1.CreateOptions{}//构造创建选项
	if p.ServerDryRun {//设置干跑
		options.DryRun = []string{metav1.DryRunAll}
	}
	createdObject, err := p.Helper.Create(namespace, true, versionedObject, &options)//创建对象到服务端
	if err != nil {
		// restore the original object if we fail to create the new one
		// but still propagate and advertise error to user
		recreated, recreateErr := p.Helper.Create(namespace, true, original, &options)//有错误重新创建
		if recreateErr != nil {
			err = fmt.Errorf("An error occurred force-replacing the existing object with the newly provided one:\n\n%v.\n\nAdditionally, an error occurred attempting to restore the original object:\n\n%v", err, recreateErr)
		} else {
			createdObject = recreated
		}
	}
	return modified, createdObject, err//返回重建成功的对象
}
//删除对象
func (p *pruner) delete(namespace, name string, mapping *meta.RESTMapping) error {
	return runDelete(namespace, name, mapping, p.dynamicClient, p.cascade, p.gracePeriod, p.serverDryRun)
}

//删除对象
func runDelete(namespace, name string, mapping *meta.RESTMapping, c dynamic.Interface, cascade bool, gracePeriod int, serverDryRun bool) error {
	options := &metav1.DeleteOptions{}//创建删除选项
	if gracePeriod >= 0 {// 如果grace-period大于0,则重新创建删除选项
		options = metav1.NewDeleteOptions(int64(gracePeriod))
	}
	if serverDryRun {//设置干跑
		options.DryRun = []string{metav1.DryRunAll}
	}
	policy := metav1.DeletePropagationForeground
	if !cascade {//设置级联策略
		policy = metav1.DeletePropagationOrphan
	}
	options.PropagationPolicy = &policy
	return c.Resource(mapping.Resource).Namespace(namespace).Delete(name, options)//执行删除
}


//删除对象
func (p *Patcher) delete(namespace, name string) error {
	return runDelete(namespace, name, p.Mapping, p.DynamicClient, p.Cascade, p.GracePeriod, p.ServerDryRun)
}
type pruner struct {//修剪结构体
	mapper        meta.RESTMapper
	dynamicClient dynamic.Interface

	visitedUids   sets.String
	labelSelector string
	fieldSelector string

	cascade      bool
	serverDryRun bool
	dryRun       bool
	gracePeriod  int

	toPrinter func(string) (printers.ResourcePrinter, error)

	out io.Writer
}

//执行修剪
func (p *pruner) prune(namespace string, mapping *meta.RESTMapping) error {
	objList, err := p.dynamicClient.Resource(mapping.Resource).
		Namespace(namespace).
		List(metav1.ListOptions{
			LabelSelector: p.labelSelector,
			FieldSelector: p.fieldSelector,
		})//获取资源列表
	if err != nil {
		return err
	}

	objs, err := meta.ExtractList(objList)//抽取对象列表
	if err != nil {
		return err
	}

	for _, obj := range objs {//遍历对象
		metadata, err := meta.Accessor(obj)//访问对象元数据
		if err != nil {
			return err
		}
		annots := metadata.GetAnnotations()//获取注解
		if _, ok := annots[corev1.LastAppliedConfigAnnotation]; !ok {//如果没有last-applied-configuation注解则跳过
			// don't prune resources not created with apply
			continue
		}
		uid := metadata.GetUID()//获取uid
		if p.visitedUids.Has(string(uid)) {//如果访问过的uid包含这个uid则跳过
			continue
		}
		name := metadata.GetName()//获取名称
		if !p.dryRun {
			if err := p.delete(namespace, name, mapping); err != nil {//删除资源
				return err
			}
		}

		printer, err := p.toPrinter("pruned")// printflag转printer
		if err != nil {
			return err
		}
		printer.PrintObj(obj, p.out)//打印结果
	}
	return nil
}

yolov8训练模型的一些心得-总结篇 先放在这,等过几天我都弄完了详细给大家说说(哈哈其实是给我自己说说,记录一下怕忘记) 阅读详情

相关推荐

手把手拆解K8s:从零部署集群到YAML与排错全攻略

容器编排是现代云原生架构的基石,而Kubernetes(简称K8s)作为事实标准,解决了大规模容器管理、服务发现、弹性伸缩和故障恢复等核心问题。其声明式API让用户通过描述期望状态来驱动系统持续收敛,这一原理贯穿于集群部署、资源对象定义和日常运维之中。理解控制平面与数据平面的角色分工、核心组件(如kubelet、etcd)的协作机制,是掌握K8s的关键。kubeadm作为官方部署工具,能帮助用户从裸机快速搭建高可用集群,同时掌握Deployment、Service等YAML清单编写技巧,以及基于kubect

weixin_31155097的博客 251

k8s实战之kubectl

本课程将详细介绍k8s所有命令,以及命令的go源码分析,学习知其然,知其所以然

K8S系列:Pod的扩缩容方法

手动扩缩容机制 scale 自动扩缩容机制 scaleTargetRef 也可以自定义一些指标 当到达指标设置阈值时,进行扩容

NIO4444 647

kubernetes基础命令

一、名称空间 资源隔离、但是网络不隔离 # 创建 kubectl create ns test #删除 kubectl delete ns test # yaml文件创建 [root@k8s-master ~]# vi test.yaml [root@k8s-master ~]# cat test.yaml apiVersion: v1 kind: Namespace metadata: name: test [root@k8s-master ~]# kubectl ap...

cbzhunian 8031

用户行为数据分析 User Behavior Data from Taobao for Recommendation

一、提出问题 二、理解数据 数据来源 阿里云天池数据集:(https://tianchi.aliyun.com/dataset/dataDetail?dataId=649) 本文选取的数据集为UserBehavior.csv,共有5242880条数据。数据的具体类别如下表所示: 用户行为种类有四种,如下表所示 对于数据维度的说明如下: 在本文选取的2017年11月25日到2017年12月3日的时间范围内,不重复的用户数量为10202,不重复的商品ID数为412369,不重复的商品类目ID数为5859,

cccxxxqq的博客 5207

更多 LSTM 变种——应用于 CMAPSS 数据集

本文并模型介绍,只是把一些比较火的技术,像双向注意力机制,自注意力机制这样的比较火的内容,简单的和LSTM堆叠,结果发现效果都不如最基础的,这里作者将本人代码及实现结果,进行展示,给各位想更改LSTM的朋友一些。

2301_77491330的博客 1766

kubectl源码分析之apply view-last-applied

发布一个k8s部署视频:https://edu.csdn.net/course/detail/26967 课程内容:各种k8s部署方式。包括minikube部署,kubeadm部署,kubeasz部署,rancher部署,k3s部署。包括开发测试环境部署k8s,和生产环境部署k8s。 腾讯课堂连接地址https://ke.qq.com/course/478827?taid=4373109931462251&tuin=ba64518 第二个视频发布https://edu.csdn.net/c..

mark's technic world 369

SpotBugs使用

spotbugs命令行参数解析

javajingling的专栏 2951

kubectl源码分析之apply set-last-applied

发布一个k8s部署视频:https://edu.csdn.net/course/detail/26967 课程内容:各种k8s部署方式。包括minikube部署,kubeadm部署,kubeasz部署,rancher部署,k3s部署。包括开发测试环境部署k8s,和生产环境部署k8s。 腾讯课堂连接地址https://ke.qq.com/course/478827?taid=4373109931462251&tuin=ba64518 第二个视频发布https://edu.csdn.net/c..

mark's technic world 457

ROS2_Moveit2_Ur5e_Grasp项目详解(二):核心模块功能解析

本文深入解析了ROS2_Moveit2_Ur5e_Grasp项目的核心模块功能。重点阐述了OctoMap建图模块如何为机械臂提供实时三维环境感知,并详细介绍了Robotiq夹爪与MoveIt2的集成控制、UR5e的运动规划策略,以及视觉模块如何协同工作,最终实现从目标检测到动态抓取的完整闭环。

weixin_29254029的博客 113

【Collibra 驱动 IPD 数据血缘治理】:三层图谱分层展示体系与全链路追踪的技术业务实践

(应用级/数据级/字段级)是理解复杂数据流动的核心手段。其价值不仅体现在可视化的直观性,更在于通过。技术保障血缘不可篡改性,进一步提升IPD数据治理的可靠性。,解决了跨层级血缘分析的性能与可用性矛盾。数据血缘图谱要素定义(字节跳动/腾讯云/信安标委)实现对IPD数据的精准追踪,而数据血缘图谱的。分层交互设计标准(智慧城市数据中台规范)Collibra技术文档(2024)字段级血缘的挑战与解决路径。自动优化血缘路径,并深化。字段级血缘技术实现方案。Collibra通过。图数据库性能优化原理。

百态·数智思维 | 聚焦大数据、量化与未来AI 1244

kubectl源码分析之apply edit-last-applied

发布一个k8s部署视频:https://edu.csdn.net/course/detail/26967 课程内容:各种k8s部署方式。包括minikube部署,kubeadm部署,kubeasz部署,rancher部署,k3s部署。包括开发测试环境部署k8s,和生产环境部署k8s。 腾讯课堂连接地址https://ke.qq.com/course/478827?taid=4373109931462251&tuin=ba64518 第二个视频发布https://edu.csdn.net/c..

mark's technic world 825

win2016 php mysql_Windows Server 2016 IIS10.0+PHP(FastCGI)+MySQL环境搭建教程 | 系统运维...

准备篇一、环境说明:操作系统:Windows Server 2016PHP版本:php 7.1.0MySQL版本:MySQL 5.7.17.0二、相关软件下载:1、PHP下载地址:2、MySQL下载地址:3、Microsoft Visual C++ 2010 可再发行组件包(安装MySQL需要此插件)4、Visual C++ Redistributable for Visual Studio 20...

weixin_34794420的博客 1378

KubeVela 多集群实战:使用 Trait 的 controlPlaneOnly 属性将资源固定派发到 Hub 集群

在 KubeVela 多集群应用编排中,业务工作负载(如 Pod、Deployment)通常需要被派发到多个子集群,而某些"控制面性质"的资源(如 HorizontalPodAutoscaler、Service 等)则更适合只创建在管理集群(Hub Cluster,KubeVela 中记为 `local`)上。KubeVela 的 Trait 定义属性 `controlPlaneOnly` 正是为

gitblog_00023的博客 150

2025年9月GESP(C++四级):排兵布阵

作为将军,你自然需要合理地排兵布阵。地图可以视为n行m列的网格,适合排兵的网格以 1 标注,不适合排兵的网格以 0 标注。现在你需要在地图上选择一个矩形区域排兵,这个矩形区域内不能包含不适合排兵的网格。请问可选择的矩形区域最多能包含多少网格?

王老师青少年编程 1386

企业数字化建设之——品牌零售全渠道运营中台解决方案

OMS审单 --> 订单进入OMS待发货列表区域 --> 备货(线下)-->拣货((线下 )-->质检(线下)--> 包装(线下)--> 快递员取货(线下)(物品 + 客户收货地址清单 )--> 快递员发货(2.2.5 正常退货,客户发起 --> 客服助理审核 --> 评审组 --> ( 货物运输 ) -->质检 ->仓库收货入 正常仓库 -->财务退款,记账。OMS审单 --> 订单进入物流公司系统(如:*丰),--> 快递员发货 --> 物流状态同步OMS --> OMS同步物流状态到销售平台。

java_zhong1990的专栏 612

【k3s】k3s 安装脚本 install.sh 超深度源码分析

k3s 安装脚本 install.sh 超深度源码分析

zhonglinzhang 495

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

KubeVela take-over 策略实战:让 Application 安全接管集群中的无主资源

> **导读**:本文围绕 KubeVela 内置的 `take-over`(接管)策略,讲解如何让一个 Application 声明式地接管集群中"不属于任何应用"的资源(例如手工 `kubectl apply` 创建的 Deployment)。你会掌握 `take-over` 策略的完整 YAML 配置、六种资源选择器字段及其 AND 匹配语义,并从源码层面理解该策略如何绕过"资源必须由应用拥

gitblog_00363的博客 367

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

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

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

Istio OpenTelemetry ALS 实战:让 Envoy 访问日志通过 OTLP 流入 OpenTelemetry Collector

本篇技术指南以 Istio 仓库中的 OpenTelemetry ALS(Access Logging Service,访问日志服务)示例为核心,讲解如何部署 OpenTelemetry Collector 作为日志后端、通过 demo 内置的 `envoyOtelAls` 扩展提供方声明导出通道,再用 `Telemetry` 资源配置网格级访问日志并落盘验证。读完本文,你将掌握一条从 Istio

gitblog_00968的博客 410

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

Prometheus Operator 故障排查完全指南:从 CRD 应用到运行期疑难杂症

Prometheus Operator 在 Kubernetes 上自动化创建、配置和管理 Prometheus 集群,但在实际部署与日常运维中,从 CRD 应用、ServiceMonitor 识别、RBAC 权限到 StatefulSet 滚动更新,都可能出现各种疑难问题。本文以 [Documentation/platform/troubleshooting.md](https://link.g

gitblog_00273的博客 267

Roxum IDE 加载本地 GGUF 大模型教程:Android 离线 AI 聊天与代码补全的秘密

Roxum IDE 是一款完全免费开源的 Android 代码编辑器与轻量级 IDE,它支持在手机上加载本地 GGUF 格式的大语言模型(LLM),实现真正的离线 AI 聊天和 AI 代码补全——模型运行在你的设备里,不用联网、不依赖云端 API。本文用 5 个步骤带你完成 Roxum IDE 加载本地 GGUF 大模型的全流程配置。 [![Roxum IDE 加载本地 GGUF 大模型的应用图

gitblog_00407的博客 255

kOps `kops toolbox addons apply` 命令完全指南:Channel 驱动的 Addon 应用与更新管理

> **导读**:本文围绕 kOps(Kubernetes Operations)中 `kops toolbox addons apply` 命令展开,深入讲解其语法、选项、执行流程、底层实现(channels 工具链与 Addons API)、版本选择机制(semver/kubernetesVersion/id/manifestHash)以及实测场景与故障排查。读完本文,你将掌握如何用该命令向集

gitblog_00101的博客 418

Argo Workflows Access Token 完整指南:为 Argo Server API 与 CLI 自动化创建、使用与撤销访问令牌

在 Kubernetes 上运行 Argo Workflows 时,如果希望由 CI/CD 流水线(如 Jenkins)、脚本或第三方系统**自动调用 Argo Server API 或 `argo` 命令行工具**,就必须提供一个可编程的访问令牌(Access Token)。本文以仓库内官方指南 [docs/access-token.md](https://link.gitcode.com/i/

gitblog_00847的博客 401

kOps `kops edit` 命令完全指南:交互式编辑与 `--set`/`--unset` 直改集群与实例组配置

kOps(Kubernetes Operations)中,`kops edit` 是修改集群(Cluster)与实例组(InstanceGroup)「期望状态」配置的官方入口。与直接编辑 state store 中的 YAML 不同,该命令会先把现有配置填充默认值、反序列化为可读 YAML 打开到编辑器,并在写入前完成赋值、填充与深度校验,避免把坏配置写进 registry。读完本文,你将掌握 `

gitblog_00255的博客 815

Operator SDK 实战指南:基于 Kubebuilder v4 的 memcached-operator 部署、发布与监控示例解析

> 导读:本文以 operator-sdk 仓库内 [memcached-operator 示例项目](https://link.gitcode.com/i/069e73d87abf8eac81622f1e0415c812) 的官方 README 为骨架,完整讲解一个 Go 语言 Operator 从环境准备、镜像构建、集群部署、资源卸载到项目分发的全流程;同时结合该示例特有的 Prometheu

gitblog_00090的博客 499

Helmfile 中 Kubedog 资源追踪的完整配置指南:trackMode、QPS/Burst 限流与日志追踪调优

Helmfile 通过 `trackMode: kubedog` 接入 kubedog 库,对 Deployment、StatefulSet、DaemonSet、Job 等资源的发布过程进行比 Helm 内置 `--wait` 更细粒度的追踪与反馈。本文以仓库根目录的 [KUBEDOG_CONFIG.md](https://link.gitcode.com/i/48ec0268d220877fb8

gitblog_00914的博客 16
上一篇: kubectl源码分析之diff
下一篇: kubectl源码分析之apply view-last-applied
hxpjava1
博客等级 码龄19年 1365粉丝 511原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hxpjava1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值