(DeepDive) kubectl get pod 를 했을때?
※ go 프로젝트의 구조를 파악하고, 언어를 눈에 익히고자는 목적으로 k8s를 코드레벨에서 deep dive해보고자 한다.
kubectl get pod를 실행했을때 상식적으로 생각했을때 호출되는 흐름은 kubectl -> auth 정보 확인 -> api server -> etcd 정도 이다. 코드레벨에서 더 자세히 확인해보자 ( 첫 시리즈니 간단한걸로 ㅎㅎ..)
참고로 v1.33.1 코드를 받아 학습했다.
kubectl를 사용하면 가장 먼저 호출되는 main함수
func main() {
logs.GlogSetter(cmd.GetLogVerbosity(os.Args)) // nolint:errcheck
command := cmd.NewDefaultKubectlCommand()
if err := cli.RunNoErrOutput(command); err != nil {
util.CheckErr(err)
}
}
- cmd 인자로 부터 GetLogVerbosity 옵션을 확인하고 log 레벨을 설정
- cmd를 기반으로 NewDefaultKubectlCommand 를 통해 초기화 진행
type KubectlOptions struct {
PluginHandler PluginHandler
Arguments []string
ConfigFlags *genericclioptions.ConfigFlags
genericiooptions.IOStreams
}
위 구조체를 기반으로
PluginHandler : CLI에 plugin을 사용하는지? 해당 plugin은 로컬에 있는지 확인하고, 없으면 무시한다.
Arguments : 공백 단위로 슬라이스되어 모든 값이 담긴다.
ConfigFlags : 아래 구조체를 가지고 NewDefaultKubectlCommand -> NewDefaultKubectlCommandWithArgs -> NewKubectlCommand 순으로 호출된다.
func NewConfigFlags(usePersistentConfig bool) *ConfigFlags {
impersonateGroup := []string{}
insecure := false
disableCompression := false
return &ConfigFlags{
Insecure: &insecure,
Timeout: utilpointer.String("0"),
KubeConfig: utilpointer.String(""),
CacheDir: utilpointer.String(getDefaultCacheDir()),
ClusterName: utilpointer.String(""),
AuthInfoName: utilpointer.String(""),
Context: utilpointer.String(""),
Namespace: utilpointer.String(""),
APIServer: utilpointer.String(""),
TLSServerName: utilpointer.String(""),
CertFile: utilpointer.String(""),
KeyFile: utilpointer.String(""),
CAFile: utilpointer.String(""),
BearerToken: utilpointer.String(""),
Impersonate: utilpointer.String(""),
ImpersonateUID: utilpointer.String(""),
ImpersonateGroup: &impersonateGroup,
DisableCompression: &disableCompression,
usePersistentConfig: usePersistentConfig,
discoveryBurst: 300,
}
}
기본값으로 먼저 셋팅 한 뒤 CLI를 AddFlags로 넘겨 args에 맞게 다시 셋팅한다.
func NewKubectlCommand(o KubectlOptions) *cobra.Command {
...
kubeConfigFlags := o.ConfigFlags
if kubeConfigFlags == nil {
kubeConfigFlags = defaultConfigFlags().WithWarningPrinter(o.IOStreams)
}
kubeConfigFlags.AddFlags(flags)
...
kubectl(클라이언트) api서버간 버전 호환성을 확인 합니다.
matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags)
matchVersionKubeConfigFlags.AddFlags(flags)
CLI에 ExplicitPath(명시적 경로)가 없을 경우 로컬 .kube/config 파일을 load 하고, config 구조체를 불러와서 kubeconfigs에 config 파일을 매핑하게 됩니다.
if len(rules.ExplicitPath) > 0 {
if _, err := os.Stat(rules.ExplicitPath); os.IsNotExist(err) {
return nil, err
}
kubeConfigFiles = append(kubeConfigFiles, rules.ExplicitPath)
} else {
kubeConfigFiles = append(kubeConfigFiles, rules.Precedence...)
}
kubeconfigs := []*clientcmdapi.Config{}
for _, filename := range kubeConfigFiles {
if len(filename) == 0 {
// no work to do
continue
}
config, err := LoadFromFile(filename)
if os.IsNotExist(err) {
missingList = append(missingList, filename)
continue
}
if err != nil {
errlist = append(errlist, fmt.Errorf("error loading config file \"%s\": %v", filename, err))
continue
}
kubeconfigs = append(kubeconfigs, config)
}
명령어 조건이 없어서 당황했는데 모든 명령어 객체를 조건 없이 생성하고 있었다.
// Avoid import cycle by setting ValidArgsFunction here instead of in NewCmdGet()
getCmd := get.NewCmdGet("kubectl", f, o.IOStreams)
...
groups := templates.CommandGroups{
{
Message: "Basic Commands (Beginner):",
Commands: []*cobra.Command{
create.NewCmdCreate(f, o.IOStreams),
expose.NewCmdExposeService(f, o.IOStreams),
run.NewCmdRun(f, o.IOStreams),
set.NewCmdSet(f, o.IOStreams),
},
},
{
Message: "Basic Commands (Intermediate):",
Commands: []*cobra.Command{
explain.NewCmdExplain("kubectl", f, o.IOStreams),
getCmd,
edit.NewCmdEdit(f, o.IOStreams),
delete.NewCmdDelete(f, o.IOStreams),
},
},
{
...
NewCmdGet 함수를 타고 들어가보면 해당 함수에 Run 콜백 함수를 지정하고 Complete, Validate, Run 메서드를 등록하고 있었다. 뒷편에서 cobra.Command를 통해 해당 함수를 호출해서 최종 리턴할 예정이다. (아직은 초기화 단계임)
func NewCmdGet(parent string, f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
o := NewGetOptions(parent, streams)
cmd := &cobra.Command{
Use: fmt.Sprintf("get [(-o|--output=)%s] (TYPE[.VERSION][.GROUP] [NAME | -l label] | TYPE[.VERSION][.GROUP]/NAME ...) [flags]", strings.Join(o.PrintFlags.AllowedFormats(), "|")),
DisableFlagsInUseLine: true,
Short: i18n.T("Display one or many resources"),
Long: getLong + "\n\n" + cmdutil.SuggestAPIResources(parent),
Example: getExample,
// ValidArgsFunction is set when this function is called so that we have access to the util package
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(o.Complete(f, cmd, args))
cmdutil.CheckErr(o.Validate())
cmdutil.CheckErr(o.Run(f, args))
},
SuggestFor: []string{"list", "ps"},
}
get, cluster-info, delete, describe 등등 명령어를 초기화하면 RunNoErrOutput 함수로 실행을 하게 된다.
쭉쭉 따라가보면 c.ExecuteC함수에서 Find 함수를 호출하고, 입력받은 CLI는 트리구조로 재귀를 통해 분석한다.
이때 Command 구조체를 sub 커맨드로 초기화 한다.
func (c *Command) Find(args []string) (*Command, []string, error) {
var innerfind func(*Command, []string) (*Command, []string)
innerfind = func(c *Command, innerArgs []string) (*Command, []string) {
argsWOflags := stripFlags(innerArgs, c)
if len(argsWOflags) == 0 {
return c, innerArgs
}
nextSubCmd := argsWOflags[0]
cmd := c.findNext(nextSubCmd)
if cmd != nil {
return innerfind(cmd, c.argsMinusFirstX(innerArgs, nextSubCmd))
}
return c, innerArgs
}
commandFound, a := innerfind(c, args)
if commandFound.Args == nil {
return commandFound, a, legacyArgs(commandFound, stripFlags(a, commandFound))
}
return commandFound, a, nil
}
사이사이 무수히 많은 사전 작업(유효성 검사)을 거치고
if c.PreRunE != nil {
if err := c.PreRunE(c, argWoFlags); err != nil {
return err
}
} else if c.PreRun != nil {
c.PreRun(c, argWoFlags)
}
if err := c.ValidateRequiredFlags(); err != nil {
return err
}
if err := c.ValidateFlagGroups(); err != nil {
return err
}
if c.RunE != nil {
if err := c.RunE(c, argWoFlags); err != nil {
return err
}
} else {
c.Run(c, argWoFlags)
}
c.Run(c, argWoFlags)가 실행되면서 NewDefaultKubectlCommand에서 초기화 하며 만들었던 sub Command에 맞는 Run 함수를 실행하게 된다.
infos, err := r.Infos() -> List 함수 -> Do 함수를 통해 send함수를 거쳐 RoundTrip에서 Bear토큰을 생성하고, 코드단에서 토큰을 Mask시킨다.
최종적으로 보내는 요청의 형태이다.
curl -v -XGET \
-H "Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json" \
-H "User-Agent: __debug_bin3155585782.exe/v0.0.0 (windows/amd64) kubernetes/$Format" \
-H "Authorization: Bearer <masked>" 'https://xxx:443/api/v1/namespaces/default/pods?limit=500'
정리한 글을 바탕으로 UML 작성(w/ cursor)
느낀점 or 배운점
golang에는 class가 없다. 구조체와 인터페이스를 조합해서 사용하는 듯하다.
cursor를 통해 쉽고 빠르게 공부할 수 있을거라 생각했지만 정확하진 않았다.
힌트를 많이줘도 정확하게 파악하진 못했고, 결국 debug모드로 한땀한땀 찾아가며 공부했다.
vscode에서 crtl + click으로 따라다녀도 엉뚱한 곳으로 가는 경우가 있었고, debug모드 짱짱맨..
사내에서 봇 서버를 만들어서 쓰는데 if 떡칠인.. 내 코드를 보고 반성을 하게 되는것도 있고,, 본받을 구조가 있는지 디테일하게 보고
고 개선해도 좋을거 같다.
네트워크 및 CNI, 신규 버전에서 ga되는 기능들 위주로 해보고싶다
사용한 launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug kubectl get pod",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "C:/workspaces/github/kubernetes/cmd/kubectl/kubectl.go",
"args": ["get", "pod"],
"cwd": "C:/workspaces/github/kubernetes",
"env": {
"KUBECONFIG": "C:/Users/xx/.kube/config"
},
"stopOnEntry": false,
"internalConsoleOptions": "openOnSessionStart",
"showLog": true
}
]
}