iam源码学习1

iam源码学习1

本文梳理学习下iam项目中iam-apiserver代码的实现,只为做个学习记录。

一.iam-apiserver的流程

  1. 首先根据目录结构了解到,cmd/下存放的是各个应用的入口,internal/下存放真实的应用代码,其中internal/pkg存放项目内可共享,不对外共享的包,对外共享的包存放在/pkg中
    internal/apiserver目录就是iam-apiserver的真实应用代码了,其目录结构如下:
    在这里插入图片描述

  2. 命令行构建流程
    从cmd/iam-apiserver/apiserver.go的main方法为入口:

func main() {
	rand.Seed(time.Now().UTC().UnixNano())
	if len(os.Getenv("GOMAXPROCS")) == 0 {
		runtime.GOMAXPROCS(runtime.NumCPU())
	}

	apiserver.NewApp("iam-apiserver").Run()
}

main方法比较简洁,调用internal/apiserver包的NewApp,生成一个应用,再调用它的Run方法运行起来iam-apiserver服务。
这里internal/apiserver的NewApp代码如下:

func NewApp(basename string) *app.App {
	opts := options.NewOptions()
	application := app.NewApp("IAM API Server",
		basename,
		app.WithOptions(opts),
		app.WithDescription(commandDesc),
		app.WithDefaultValidArgs(),
		app.WithRunFunc(run(opts)),
	)

	return application
}

这里有两个动作,第一步是NewOptions,第二步是NewApp

  • 先看第一步,options.NewOptions(), 初始化一个Options对象,Options配置是用来构建命令行参数的,它的值来自于命令行选项或配置文件(也可以是两者merge后的配置)
func NewOptions() *Options {
	o := Options{
		GenericServerRunOptions: genericoptions.NewServerRunOptions(),
		GRPCOptions:             genericoptions.NewGRPCOptions(),
		InsecureServing:         genericoptions.NewInsecureServingOptions(),
		SecureServing:           genericoptions.NewSecureServingOptions(),
		MySQLOptions:            genericoptions.NewMySQLOptions(),
		RedisOptions:            genericoptions.NewRedisOptions(),
		JwtOptions:              genericoptions.NewJwtOptions(),
		Log:                     log.NewOptions(),
		FeatureOptions:          genericoptions.NewFeatureOptions(),
	}

	return &o
}

NewOptions初始化一个Options对象,分组并且设置了一些默认参数,这些默认参数是apiserver通用配置,因此存放的路径是在internal/pkg/options包
在这里插入图片描述

每一个选项都是独立一个文件,这里以insecureServing为例查看下源码:

type InsecureServingOptions struct {
	BindAddress string `json:"bind-address" mapstructure:"bind-address"`
	BindPort    int    `json:"bind-port"    mapstructure:"bind-port"`
}
func NewInsecureServingOptions() *InsecureServingOptions {
	return &InsecureServingOptions{
		BindAddress: "127.0.0.1",
		BindPort:    8080,
	}
}

每一个都定义了一个struct,成员是对应的配置,在初始化时设置了默认的值

  • 第二步,NewApp,调用的是pkg/app包下的方法,因为这个app构建是可重用的,可以对外共享,因此作者放在了pkg/包下了,并且这里使用了设计模式中的选项模式,将第一步的options传入了NewApp中
func NewApp(name string, basename string, opts ...Option) *App {
	a := &App{
		name:     name,
		basename: basename,
	}

	for _, o := range opts {
		o(a)
	}

	a.buildCommand()

	return a
}

NewApp内实例化了一个App,填充App结构,最后调用buildCommand构建命令:

func (a *App) buildCommand() {
	cmd := cobra.Command{
		Use:   FormatBaseName(a.basename),
		Short: a.name,
		Long:  a.description,
		// stop printing usage when the command errors
		SilenceUsage:  true,
		SilenceErrors: true,
		Args:          a.args,
	}
	// cmd.SetUsageTemplate(usageTemplate)
	cmd.SetOut(os.Stdout)
	cmd.SetErr(os.Stderr)
	cmd.Flags().SortFlags = true
	cliflag.InitFlags(cmd.Flags())

	if len(a.commands) > 0 {
		for _, command := range a.commands {
			cmd.AddCommand(command.cobraCommand())
		}
		cmd.SetHelpCommand(helpCommand(FormatBaseName(a.basename)))
	}
	if a.runFunc != nil {
		cmd.RunE = a.runCommand
	}

	var namedFlagSets cliflag.NamedFlagSets
	if a.options != nil {
		namedFlagSets = a.options.Flags()
		fs := cmd.Flags()
		for _, f := range namedFlagSets.FlagSets {
			fs.AddFlagSet(f)
		}
	}

	if !a.noVersion {
		verflag.AddFlags(namedFlagSets.FlagSet("global"))
	}
	if !a.noConfig {
		addConfigFlag(a.basename, namedFlagSets.FlagSet("global"))
	}
	globalflag.AddGlobalFlags(namedFlagSets.FlagSet("global"), cmd.Name())
	// add new global flagset to cmd FlagSet
	cmd.Flags().AddFlagSet(namedFlagSets.FlagSet("global"))

	addCmdTemplate(&cmd, namedFlagSets)
	a.cmd = &cmd
}

buildCommand使用cobra框架构建命令,将options转为cobra的flag,这里回到第一步的options里,options实现了pkg/app/options.go里的接口CliOptions(这里接口的目的应该是App结构是在pkg包,对外共享的,Options结构是在internal目录下的,因此pkg包下应该有自己的options接口,其他包的只需要实现这个接口)
这个CliOptions的接口方法是:Flags() (fss cliflag.NamedFlagSets),返回一个cliflag.NamedFlagSets对象,这个是作者开源的一个包,内部是使用pflag,保存所有的FlagSets:

type NamedFlagSets struct {
	// Order is an ordered list of flag set names.
	Order []string
	// FlagSets stores the flag sets by name.
	FlagSets map[string]*pflag.FlagSet
}

再来看看第一步里的options结构实现的Flags(fss cliflag.NamedFlagSets)方法:

func (o *Options) Flags() (fss cliflag.NamedFlagSets) {
	o.GenericServerRunOptions.AddFlags(fss.FlagSet("generic"))
	o.JwtOptions.AddFlags(fss.FlagSet("jwt"))
	o.GRPCOptions.AddFlags(fss.FlagSet("grpc"))
	o.MySQLOptions.AddFlags(fss.FlagSet("mysql"))
	o.RedisOptions.AddFlags(fss.FlagSet("redis"))
	o.FeatureOptions.AddFlags(fss.FlagSet("features"))
	o.InsecureServing.AddFlags(fss.FlagSet("insecure serving"))
	o.SecureServing.AddFlags(fss.FlagSet("secure serving"))
	o.Log.AddFlags(fss.FlagSet("logs"))

	return fss
}

这里先使用cliflag.NamedFlagSets创建一个pflag的FlagSet,并且将FlagSet组名顺序存储起来,然后再将每个组的options添加到flag中,比如:

func (s *InsecureServingOptions) AddFlags(fs *pflag.FlagSet) {
	fs.StringVar(&s.BindAddress, "insecure.bind-address", s.BindAddress, ""+
		"The IP address on which to serve the --insecure.bind-port "+
		"(set to 0.0.0.0 for all IPv4 interfaces and :: for all IPv6 interfaces).")
	fs.IntVar(&s.BindPort, "insecure.bind-port", s.BindPort, ""+
		"The port on which to serve unsecured, unauthenticated access. It is assumed "+
		"that firewall rules are set up such that this port is not reachable from outside of "+
		"the deployed machine and that port 443 on the iam public address is proxied to this "+
		"port. This is performed by nginx in the default setup. Set to zero to disable.")
}

buildCommand中这段代码:

var namedFlagSets cliflag.NamedFlagSets
if a.options != nil {
    namedFlagSets = a.options.Flags()
    fs := cmd.Flags()
    for _, f := range namedFlagSets.FlagSets {
        fs.AddFlagSet(f)
    }
}

这样就完成了使用options构建命令行参数了。
然后没有设置version和config,则给App添加flag,组为global。

  1. 应用配置构建流程
    在main方法中最后调用的是App的Run方法:apiserver.NewApp("iam-apiserver").Run(),Run方法是启动应用的方法:
func (a *App) Run() {
	if err := a.cmd.Execute(); err != nil {
		fmt.Printf("%v %v\n", color.RedString("Error:"), err)
		os.Exit(1)
	}
}

实质调用的就是cobra提供的Execute()方法,那么cmd的Run或者RunE方法就会被调用了,再往回找cmd的Run/RunE方法,在buildCommand中有:

if a.runFunc != nil {
    cmd.RunE = a.runCommand
}

再往上找,a.runCommand是通过选项模式配置进去的,app.WithRunFunc(run(opts)),,调用的是run(opts),也就是应用启动起来跑的就是run方法了:

func run(opts *options.Options) app.RunFunc {
	return func(basename string) error {
		log.Init(opts.Log)
		defer log.Flush()

		cfg, err := config.CreateConfigFromOptions(opts)
		if err != nil {
			return err
		}

		return Run(cfg)
	}
}

这里有三步:首先初始化了log,方便后续记录log,然后是调用CreateConfigFromOptions通过options创建应用的配置,最后调用Run(cfg),将应用配置传递进去,启动应用。

  • 第一步: 初始化log,使用了pkg/log包,这里先不详细看
  • 第二步:调用CreateConfigFromOptions创建应用配置,应用配置和Options配置其实是完全独立的,二者可能完全不同,但在iam-apiserver中,二者配置是相同的
type Config struct {
	*options.Options
}

// CreateConfigFromOptions creates a running configuration instance based
// on a given IAM pump command line or configuration file option.
func CreateConfigFromOptions(opts *options.Options) (*Config, error) {
	return &Config{opts}, nil
}
  • 第三步:调用Run(cfg *config.Config)方法,真正运行iam-apiserver:
func Run(cfg *config.Config) error {
	server, err := createAPIServer(cfg)
	if err != nil {
		return err
	}

	return server.PrepareRun().Run()
}

Run方法也分为了几部分:

  1. 首先createAPIServer通过配置创建一个apiserver实例:
type apiServer struct {
	gs               *shutdown.GracefulShutdown
	redisOptions     *genericoptions.RedisOptions
	gRPCAPIServer    *grpcAPIServer
	genericAPIServer *genericapiserver.GenericAPIServer
}
func createAPIServer(cfg *config.Config) (*apiServer, error) {
	gs := shutdown.New()
	gs.AddShutdownManager(posixsignal.NewPosixSignalManager())

	genericConfig, err := buildGenericConfig(cfg)
	if err != nil {
		return nil, err
	}

	extraConfig, err := buildExtraConfig(cfg)
	if err != nil {
		return nil, err
	}

	genericServer, err := genericConfig.Complete().New()
	if err != nil {
		return nil, err
	}
	extraServer, err := extraConfig.complete().New()
	if err != nil {
		return nil, err
	}

	server := &apiServer{
		gs:               gs,
		redisOptions:     cfg.RedisOptions,
		genericAPIServer: genericServer,
		gRPCAPIServer:    extraServer,
	}

	return server, nil
}

其中,shutdown.GracefulShutdown是跟优雅关停服务有关的,暂且略过;剩下的就是两个server:genericAPIServer和gRPCAPIServer,通过传入的应用配置分别创建HTTP/GRPC配置.

  • buildGenericConfig() 创建HTTP相关的配置
func buildGenericConfig(cfg *config.Config) (genericConfig *genericapiserver.Config, lastErr error) {
	genericConfig = genericapiserver.NewConfig()
	if lastErr = cfg.GenericServerRunOptions.ApplyTo(genericConfig); lastErr != nil {
		return
	}

	if lastErr = cfg.FeatureOptions.ApplyTo(genericConfig); lastErr != nil {
		return
	}

	if lastErr = cfg.SecureServing.ApplyTo(genericConfig); lastErr != nil {
		return
	}

	if lastErr = cfg.InsecureServing.ApplyTo(genericConfig); lastErr != nil {
		return
	}

	return
}

这里首先server有自己的配置:

type Config struct {
	SecureServing   *SecureServingInfo
	InsecureServing *InsecureServingInfo
	Jwt             *JwtInfo
	Mode            string
	Middlewares     []string
	Healthz         bool
	EnableProfiling bool
	EnableMetrics   bool
}

然后应用配置(这里和Options一样)的ApplyTo方法,实际上就是将应用配置的数据赋值给Sever配置,随便选一个查看源码:

// ApplyTo applies the run options to the method receiver and returns self.
func (s *ServerRunOptions) ApplyTo(c *server.Config) error {
	c.Mode = s.Mode
	c.Healthz = s.Healthz
	c.Middlewares = s.Middlewares

	return nil
}

这样就实现了从Options配置->应用配置->服务配置

  • buildExtraConfig() 创建GRPC相关的配置
// ExtraConfig defines extra configuration for the iam-apiserver.
type ExtraConfig struct {
	Addr         string
	MaxMsgSize   int
	ServerCert   genericoptions.GeneratableKeyCert
	mysqlOptions *genericoptions.MySQLOptions
	// etcdOptions      *genericoptions.EtcdOptions
}
func buildExtraConfig(cfg *config.Config) (*ExtraConfig, error) {
	return &ExtraConfig{
		Addr:         fmt.Sprintf("%s:%d", cfg.GRPCOptions.BindAddress, cfg.GRPCOptions.BindPort),
		MaxMsgSize:   cfg.GRPCOptions.MaxMsgSize,
		ServerCert:   cfg.SecureServing.ServerCert,
		mysqlOptions: cfg.MySQLOptions,
		// etcdOptions:      cfg.EtcdOptions,
	}, nil
}

最后再调用Complete()补全配置,再调用补全配置后的结构New一个实例

type CompletedConfig struct {
	*Config
}
func (c *Config) Complete() CompletedConfig {
	return CompletedConfig{c}
}

type completedExtraConfig struct {
	*ExtraConfig
}
func (c *ExtraConfig) complete() *completedExtraConfig {
	if c.Addr == "" {
		c.Addr = "127.0.0.1:8081"
	}

	return &completedExtraConfig{c}
}

这里补全配置并没有做太多,在实际的Go项目开发中,我们需要提供一种机制来处理或补全配置,这在Go项目开发中是一个非常有用的步骤。

这里有个设计技巧:complete函数返回的是一个*completedExtraConfig类型的实例,在创建GRPC实例时,是调用completedExtraConfig结构体提供的New方法,这种设计方法可以确保我们创建的GRPC实例一定是基于complete之后的配置(completed)
最后New()方法分别根据配置创建一个服务的实例:

func (c CompletedConfig) New() (*GenericAPIServer, error) {
	// setMode before gin.New()
	gin.SetMode(c.Mode)

	s := &GenericAPIServer{
		SecureServingInfo:   c.SecureServing,
		InsecureServingInfo: c.InsecureServing,
		healthz:             c.Healthz,
		enableMetrics:       c.EnableMetrics,
		enableProfiling:     c.EnableProfiling,
		middlewares:         c.Middlewares,
		Engine:              gin.New(),
	}

	initGenericAPIServer(s)

	return s, nil
}

func (c *completedExtraConfig) New() (*grpcAPIServer, error) {
	creds, err := credentials.NewServerTLSFromFile(c.ServerCert.CertKey.CertFile, c.ServerCert.CertKey.KeyFile)
	if err != nil {
		log.Fatalf("Failed to generate credentials %s", err.Error())
	}
	opts := []grpc.ServerOption{grpc.MaxRecvMsgSize(c.MaxMsgSize), grpc.Creds(creds)}
	grpcServer := grpc.NewServer(opts...)

	storeIns, _ := mysql.GetMySQLFactoryOr(c.mysqlOptions)
	// storeIns, _ := etcd.GetEtcdFactoryOr(c.etcdOptions, nil)
	store.SetClient(storeIns)
	cacheIns, err := cachev1.GetCacheInsOr(storeIns)
	if err != nil {
		log.Fatalf("Failed to get cache instance: %s", err.Error())
	}

	pb.RegisterCacheServer(grpcServer, cacheIns)

	reflection.Register(grpcServer)

	return &grpcAPIServer{grpcServer, c.Addr}, nil
}
  1. 通过createAPIServer创建了apiserver实例后,调用server.PrepareRun().Run()启动服务
func (s *apiServer) PrepareRun() preparedAPIServer {
	initRouter(s.genericAPIServer.Engine)

	s.initRedisStore()

	s.gs.AddShutdownCallback(shutdown.ShutdownFunc(func(string) error {
		mysqlStore, _ := mysql.GetMySQLFactoryOr(nil)
		if mysqlStore != nil {
			_ = mysqlStore.Close()
		}

		s.gRPCAPIServer.Close()
		s.genericAPIServer.Close()

		return nil
	}))

	return preparedAPIServer{s}
}
func (s preparedAPIServer) Run() error {
	go s.gRPCAPIServer.Run()

	// start shutdown managers
	if err := s.gs.Start(); err != nil {
		log.Fatalf("start shutdown manager failed: %s", err.Error())
	}

	return s.genericAPIServer.Run()
}

二.总结

  1. iam-apiserver有三种配置:Options配置,应用配置,HTTP/GRPC服务配置
    在这里插入图片描述

三种配置的关系如下:
在这里插入图片描述

Options配置接管命令行选项,应用配置接管整个应用的配置,HTTP/GRPC服务配置接管跟HTTP/GRPC服务相关的配置。这3种配置独立开来,可以解耦命令行选项、应用和应用内的服务,使得这3个部分可以独立扩展,又不相互影响。
2. iam-apiserver的启动流程设计
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值