github.com/machinefi/w3bstream@v1.6.5-rc9.0.20240426031326-b8c7c4876e72/pkg/depends/conf/app/app_ctx.go (about)

     1  package app
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"reflect"
    11  	"strings"
    12  
    13  	"github.com/fatih/color"
    14  	"github.com/pkg/errors"
    15  	"github.com/spf13/cobra"
    16  	"gopkg.in/yaml.v2"
    17  
    18  	"github.com/machinefi/w3bstream/pkg/depends/base/consts"
    19  	"github.com/machinefi/w3bstream/pkg/depends/base/types"
    20  	"github.com/machinefi/w3bstream/pkg/depends/conf/deploy"
    21  	"github.com/machinefi/w3bstream/pkg/depends/conf/env"
    22  	"github.com/machinefi/w3bstream/pkg/depends/x/misc/must"
    23  	"github.com/machinefi/w3bstream/pkg/depends/x/reflectx"
    24  )
    25  
    26  type Ctx struct {
    27  	cmd       *cobra.Command
    28  	name      string          // name app name
    29  	feat      string          // feat git feature
    30  	version   string          // version git version|git tag
    31  	root      string          // root app root
    32  	vars      []*env.Vars     // vars default env vars
    33  	conf      []reflect.Value // conf config reflect.Value
    34  	deployers map[string]deploy.Deployer
    35  	ctx       context.Context
    36  }
    37  
    38  func New(setters ...OptSetter) *Ctx {
    39  	c := &Ctx{ctx: context.Background()}
    40  	for _, setter := range setters {
    41  		setter(c)
    42  	}
    43  	c.cmd = &cobra.Command{}
    44  	if feat, ok := os.LookupEnv(consts.EnvProjectFeat); ok && feat != "" {
    45  		c.feat = feat
    46  	}
    47  	if version, ok := os.LookupEnv(consts.EnvProjectVersion); ok && version != "" {
    48  		c.version = version
    49  	}
    50  	if name, ok := os.LookupEnv(consts.EnvProjectName); ok && name != "" {
    51  		c.name = name
    52  	}
    53  	_ = os.Setenv(consts.EnvProjectName, c.name)
    54  	return c
    55  }
    56  
    57  func (c *Ctx) Context() context.Context { return c.ctx }
    58  
    59  // Conf init all configs from yml file, and do initialization for each config.
    60  // config dir include `config.yml.template` `config.yml` and `master.yml`
    61  // config.yml.template shows config file template and preset config values
    62  // config.yml contains all user configured values
    63  func (c *Ctx) Conf(configs ...interface{}) {
    64  	local, err := os.ReadFile(filepath.Join(c.root, "./config/config.yml"))
    65  	if err == nil {
    66  		kv := make(map[string]string)
    67  		log.Println("set config to to environ")
    68  		if err = yaml.Unmarshal(local, &kv); err == nil {
    69  			for k, v := range kv {
    70  				_ = os.Setenv(k, v)
    71  				log.Println("set config", k, v)
    72  			}
    73  		}
    74  	}
    75  
    76  	if key := os.Getenv(consts.GoRuntimeEnv); key == "" {
    77  		_ = os.Setenv(consts.GoRuntimeEnv, consts.ProduceEnv)
    78  	}
    79  
    80  	for _, v := range configs {
    81  		rv := reflect.ValueOf(v)
    82  		if rv.Kind() != reflect.Ptr {
    83  			panic("should pass pointer for setting value")
    84  		}
    85  
    86  		must.NoError(c.scan(rv))
    87  		must.NoError(c.marshal(rv))
    88  	}
    89  
    90  	if err = c.MarshalDefault(); err != nil {
    91  		panic(err)
    92  	}
    93  
    94  	for _, v := range configs {
    95  		rv := reflect.ValueOf(v)
    96  		c.conf = append(c.conf, rv)
    97  
    98  		if zero, ok := v.(types.ZeroChecker); ok && zero.IsZero() {
    99  			t := reflect.Indirect(reflect.ValueOf(v)).Type()
   100  			log.Println(errors.Errorf(
   101  				"zero config: %s",
   102  				color.CyanString("[%s.%s]:", filepath.Base(t.PkgPath()), t.Name()),
   103  			))
   104  		}
   105  
   106  		switch conf := v.(type) {
   107  		case interface{ Init() }:
   108  			conf.Init()
   109  		case interface{ Init() error }:
   110  			if err = conf.Init(); err != nil {
   111  				panic(errors.Errorf("conf init: %v", err))
   112  			}
   113  		}
   114  
   115  		rv = reflectx.Indirect(rv)
   116  		if rv.Kind() == reflect.Struct {
   117  			for i := 0; i < rv.NumField(); i++ {
   118  				value := rv.Field(i)
   119  				if !value.CanInterface() {
   120  					continue
   121  				}
   122  				if value.Type().Kind() == reflect.Interface {
   123  					panic("interface type unsupported in config scanning")
   124  				}
   125  				fv := value.Interface()
   126  				ft := reflect.Indirect(reflect.ValueOf(fv)).Type()
   127  				if zero, ok := fv.(types.ZeroChecker); ok && zero.IsZero() {
   128  					log.Println(errors.Errorf(
   129  						"zero config: %s",
   130  						color.CyanString("[%s.%s]:", filepath.Base(ft.PkgPath()), ft.Name()),
   131  					))
   132  					continue
   133  				}
   134  				switch conf := value.Interface().(type) {
   135  				case interface{ Init() }:
   136  					conf.Init()
   137  				case interface{ Init() error }:
   138  					if err = conf.Init(); err != nil {
   139  						panic(errors.Errorf("init failed %s %s",
   140  							color.CyanString("[%s.%s]:", filepath.Base(ft.PkgPath()), ft.Name()),
   141  							color.RedString("[%v]", err),
   142  						))
   143  					}
   144  				}
   145  			}
   146  		}
   147  	}
   148  }
   149  
   150  func (c *Ctx) AddCommand(name string, fn func(...string), commands ...func(*cobra.Command)) {
   151  	cmd := &cobra.Command{Use: name}
   152  
   153  	for i := range commands {
   154  		commands[i](cmd)
   155  	}
   156  
   157  	cmd.Run = func(_ *cobra.Command, args []string) {
   158  		fn(args...)
   159  	}
   160  
   161  	c.cmd.AddCommand(cmd)
   162  }
   163  
   164  func (c *Ctx) String() string {
   165  	ret := c.name
   166  	if c.feat != "" {
   167  		ret += "--" + c.feat
   168  	}
   169  	if c.version != "" {
   170  		ret += "@" + c.version
   171  	}
   172  	return ret
   173  }
   174  
   175  func (c *Ctx) Root() string { return c.root }
   176  
   177  func (c *Ctx) Execute(fn func(...string), commands ...func(*cobra.Command)) {
   178  	for i := range commands {
   179  		commands[i](c.cmd)
   180  	}
   181  	c.cmd.Use = c.name
   182  	c.cmd.Version = c.version
   183  	c.cmd.Run = func(cmd *cobra.Command, args []string) {
   184  		for i := range c.conf {
   185  			c.log(c.conf[i])
   186  		}
   187  		fn(args...)
   188  	}
   189  	// TODO implement app deploy config generator
   190  	// for name, dpl := range c.deployers {
   191  	// 	c.AddCommand(name, func(...string) {
   192  	// 		if setter, ok := dpl.(types.DefaultSetter); ok {
   193  	// 			setter.SetDefault()
   194  	// 		}
   195  	// 		filename := path.Join(c.root, name)
   196  	// 		if err := dpl.Write(filename); err != nil {
   197  	// 			panic(fmt.Errorf("init %s error: %v", name, err))
   198  	// 		}
   199  	// 	}, func(cmd *cobra.Command) {
   200  	// 		cmd.Short = "init configuration for " + name
   201  	// 	})
   202  	// }
   203  	if err := c.cmd.Execute(); err != nil {
   204  		panic(err)
   205  	}
   206  }
   207  
   208  func (c *Ctx) scan(rv reflect.Value) error {
   209  	vars := env.NewVars(c.group(rv))
   210  
   211  	if err := env.NewDecoder(vars).Decode(rv); err != nil {
   212  		return err
   213  	}
   214  	c.vars = append(c.vars, vars)
   215  	if _, err := env.NewEncoder(vars).Encode(rv); err != nil {
   216  		return err
   217  	}
   218  	return nil
   219  }
   220  
   221  func (c *Ctx) marshal(rv reflect.Value) error {
   222  	log.Println("load environments .....")
   223  	vars := env.LoadVarsFromEnviron(c.group(rv), os.Environ())
   224  	if err := env.NewDecoder(vars).Decode(rv); err != nil {
   225  		return err
   226  	}
   227  	return nil
   228  }
   229  
   230  func (c *Ctx) MarshalDefault() error {
   231  	// TODO: add comment for each single config element
   232  	m := map[string]string{
   233  		consts.GoRuntimeEnv: consts.ProduceEnv,
   234  	}
   235  	for _, vars := range c.vars {
   236  		for _, v := range vars.Values {
   237  			if !v.Optional {
   238  				m[v.Key(vars.Prefix)] = v.Value
   239  			}
   240  		}
   241  	}
   242  
   243  	return WriteYamlFile(path.Join(c.root, "./config/config.yml.template"), m)
   244  }
   245  
   246  func (c *Ctx) log(rv reflect.Value) {
   247  	vars := env.NewVars(c.group(rv))
   248  	if _, err := env.NewEncoder(vars).Encode(rv); err != nil {
   249  		panic(err)
   250  	}
   251  	fmt.Printf("%s", string(vars.MaskBytes()))
   252  }
   253  
   254  type Marshaller func(v interface{}) ([]byte, error)
   255  
   256  // group returns config group name
   257  func (c *Ctx) group(rv reflect.Value) string {
   258  	group := rv.Elem().Type().Name()
   259  	if rv.Elem().Type().Implements(types.RTypeNamed) {
   260  		group = rv.Elem().Interface().(types.Named).Name()
   261  	}
   262  	if group == "" {
   263  		return strings.ToUpper(strings.Replace(c.name, "-", "_", -1))
   264  	}
   265  	return strings.ToUpper(strings.Replace(c.name+"__"+group, "-", "_", -1))
   266  }