iam源码学习1
本文梳理学习下iam项目中iam-apiserver代码的实现,只为做个学习记录。
一.iam-apiserver的流程
-
首先根据目录结构了解到,cmd/下存放的是各个应用的入口,internal/下存放真实的应用代码,其中internal/pkg存放项目内可共享,不对外共享的包,对外共享的包存放在/pkg中
internal/apiserver目录就是iam-apiserver的真实应用代码了,其目录结构如下:
-
命令行构建流程
从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。
- 应用配置构建流程
在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方法也分为了几部分:
- 首先
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
}
- 通过
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()
}
二.总结
- iam-apiserver有三种配置:Options配置,应用配置,HTTP/GRPC服务配置
三种配置的关系如下:
Options配置接管命令行选项,应用配置接管整个应用的配置,HTTP/GRPC服务配置接管跟HTTP/GRPC服务相关的配置。这3种配置独立开来,可以解耦命令行选项、应用和应用内的服务,使得这3个部分可以独立扩展,又不相互影响。
2. iam-apiserver的启动流程设计