分析harbor容器的docker-compose.yml文件生成过程

Posted by ZMY on February 24, 2021

分析harbor容器的docker-compose.yml文件生成过程

根据官网文档安装harbor的过程中发现,harbor容器的安装过程极为简单,主要涉及几个操作

  • 解压缩harbor-online压缩包
  • 修改barbor.yml配置文件
  • 执行install.sh脚本

项目执行前目录结构如下

harbor
├── common.sh
├── harbor.yml.tmpl
├── install.sh
├── LICENSE
└── prepare

项目执行后目录结构如下

.
├── common
│   └── config
│       ├── core
│       │   ├── app.conf
│       │   ├── certificates
│       │   └── env
│       ├── db
│       │   └── env
│       ├── jobservice
│       │   ├── config.yml
│       │   └── env
│       ├── log
│       │   ├── logrotate.conf
│       │   └── rsyslog_docker.conf
│       ├── nginx
│       │   ├── conf.d
│       │   └── nginx.conf
│       ├── registry
│       │   ├── config.yml
│       │   ├── passwd
│       │   └── root.crt
│       ├── registryctl
│       │   ├── config.yml
│       │   └── env
│       └── shared
│           └── trust-certificates
├── common.sh
├── docker-compose.yml
├── harbor.yml
├── harbor.yml.tmpl
├── install.sh
├── LICENSE
└── prepare

可以看到项目执行前并不存在docker-compose.yml文件,项目执行后就生成了一些配置文件,其中包括了docker-compose.yml,那么docker-compose.yml是如何生成的呢。查看install.sh脚本并未找到docker-compose.yml和其他配置文件的生成过程。但是其中 87行 ./prepare $prepare_para 引起了我的注意,显然install.sh脚本在执行docker-compose up 之前执行了prepare 命令来生成了一些东西,那有可能是prepare脚本生成的docker-compose.yml这个文件,打开prepare看看。很遗憾没有找到docker-compose.yml生成的相关信息,但是却找到了一个 docker run命令生成了一个prepare容器,并且该容器运行后立刻删除

# docker run --rm -v $input_dir:/input:z \
                    -v $data_path:/data:z \
                    -v $harbor_prepare_path:/compose_location:z \
                    -v $config_dir:/config:z \
                    -v /:/hostfs:z \
                    goharbor/prepare:v2.0.6 prepare $@

显然答案就在这个容器中了,使用如下命令创建prepare容器,查看一下入口命令

# docker run -it goharbor/prepare:v2.0.6 prepare
# docker ps -a --no-trunc | grep prepare

容器最后入口命令是python main.py prepare,显然那些不存在的配置文件就是由这个命令搞出来的,接下来创建prepare容器并copy出main.py所在项目程序到本地进行分析

# docker run -it --entrypoint /bin/bash goharbor/prepare:v2.0.6
# docker cp `docker ps -a | grep prepare | awk '{print $1}'`:/usr/src/app ./
# cd app

列出main.py脚本所在目录结构,其中不必要的文件已删除

├── commands
│   ├── gencerts.py
│   ├── __init__.py
│   ├── migrate.py
│   ├── prepare.py
├── g.py
├── __init__.py
├── main.py
├── migrations
│   ├── __init__.py
│   └── version_2_0_0
│       ├── harbor.yml.jinja
│       └── __init__.py
├── models.py
├── scripts
│   └── gencert.sh
├── templates
│   ├── chartserver
│   │   └── env.jinja
│   ├── clair
│   │   ├── clair_env.jinja
│   │   ├── config.yaml.jinja
│   │   ├── postgres_env.jinja
│   │   └── postgresql-init.d
│   │       └── README.md
│   ├── clair-adapter
│   │   └── env.jinja
│   ├── core
│   │   ├── app.conf.jinja
│   │   └── env.jinja
│   ├── db
│   │   └── env.jinja
│   ├── docker_compose
│   │   └── docker-compose.yml.jinja
│   ├── jobservice
│   │   ├── config.yml.jinja
│   │   └── env.jinja
│   ├── log
│   │   ├── logrotate.conf.jinja
│   │   └── rsyslog_docker.conf.jinja
│   ├── nginx
│   │   ├── nginx.http.conf.jinja
│   │   ├── nginx.https.conf.jinja
│   │   ├── notary.server.conf.jinja
│   │   └── notary.upstream.conf.jinja
│   ├── notary
│   │   ├── server-config.postgres.json.jinja
│   │   ├── server_env.jinja
│   │   ├── signer-config.postgres.json.jinja
│   │   └── signer_env.jinja
│   ├── registry
│   │   └── config.yml.jinja
│   ├── registryctl
│   │   ├── config.yml.jinja
│   │   └── env.jinja
│   └── trivy-adapter
│       └── env.jinja
├── tests
│   └── migrations
│       └── utils_test.py
├── utils
│   ├── cert.py
│   ├── chart.py
│   ├── clair_adapter.py
│   ├── clair.py
│   ├── configs.py
│   ├── core.py
│   ├── db.py
│   ├── docker_compose.py
│   ├── __init__.py
│   ├── internal_tls.py
│   ├── jinja.py
│   ├── jobservice.py
│   ├── log.py
│   ├── migration.py
│   ├── misc.py
│   ├── nginx.py
│   ├── notary.py
│   ├── proxy.py
│   ├── redis.py
│   ├── registry_ctl.py
│   ├── registry.py
│   └── trivy_adapter.py
└── versions

下面以docker-compose.yml文件生成为例,分析一下文件生成过程,其他文件同理

1.程序主入口main.py

2.click装饰器分析命令行参数

3.prepare函数一阶段根据harbor.yml获取config_dict中必要参数

4.prepare函数二阶段根据config_dict值使用jinjia2模块渲染docker-compose.yml.jinja文件生成docker-compose.yml文件用于部署harbor容器

因为这里只分析prepare执行过程,因此将不必要的代码注释掉

main.py内容如下

from commands.prepare import prepare
#from commands.gencerts import gencert
#from commands.migrate import migrate
import click

@click.group()
def cli():
   pass

cli.add_command(prepare)
#cli.add_command(gencert)
#cli.add_command(migrate)

if __name__ == '__main__':
   cli()

from commands.prepare import prepare

—->prepare.py :29-35行

29 @click.command()
30 @click.option('--conf', default=input_config_path, help="the path of Harbor configuration file")
31-34 ******
35 def prepare(conf, with_notary, with_clair, with_trivy, with_chartmuseum):

以上代码相当于prepare=click.command(click.option(prepare)),下面分成2段解析代码执行过程

1)第一段如下,这里涉及到了装饰器,对装饰器有疑问的可以查看官方文档进行解读

f=click.option(‘–conf’, default=input_config_path, help=”the path of Harbor configuration file”)(prepare)

—>decorators.py:175行

跳进option函数开始,这一阶段主要功能是将conf参数包装成Option类实例,保存到f的__click_params__属性中

f代指prepare函数

def option(*param_decls, **attrs): # param_decls: --conf attrs: {default=input_config_path, help="the path of Harbor configuration file"}

—>decorators.py:186行

def decorator(f):     #params: f:<function prepare>

—>decorators.py:196行

return decorator

—->decorator.py:192行

设置OptionClass为Option类

OptionClass = option_attrs.pop("cls", Option)  #OptionClass: <class Option>

—->decorator.py:193行

_param_memo(f, OptionClass(param_decls, **option_attrs))

调用OptionClass(param_decls, **option_attrs),即调用Option()

​ —->core.py:1654行

class Option(Parameter):

​ —->core.py:1715行

Parameter.__init__(self, param_decls, type=type, **attrs)  

​ —->core.py:1483行

设置self.name self.opts属性值

self.name, self.opts, self.secondary_opts = self._parse_decls(
            param_decls or (), expose_value)     # self:<class Option: conf>  self.name: conf  self.opts: '--conf'

​ —->core.py:1782行

def _parse_decls(self, decls, expose_value):   

​ —->core.py:1824行

return name, opts, secondary_opts    # name:{str} 'conf'   opts:{list} ['--conf']

​ —->decorator.py:147行

def _param_memo(f, param):

​ —->decorator.py:153行

f.__click_params__.append(param)

给f的__click_params__属性添加 conf参数,conf为被装饰的Option类

2)prepare=click.command(f)

这段代码主要是将prepare包装成Command实例,并附加conf参数

—->decorators.py:108行

def command(name=None, cls=None, **attrs):

—->decorators.py:129行

对cls赋值为Command类

cls = Command 

—->decorators.py:132行

cmd = _make_command(f, name, attrs, cls)

​ —->decorators.py:84行

click_params” 保存着之前option()命令添加的 Option类的实例conf

params = f.__click_params__

—->decorators.py:99-104行

返回实例化Command类后的值

return cls(
    name=name or f.__name__.lower().replace("_", "-"),
    callback=f,
    params=params,
    **attrs
)

​ —->core.py:833行

class Command(BaseCommand):

​ —->core.py:881行

BaseCommand.__init__(self, name, context_settings)

3)

@click.group() def cli(): pass

—->decorators.py: 139行

def group(name=None, **attrs):

—->decorators.py: 144行

将cls设置为Group类

attrs.setdefault("cls", Group)

—->decorators.py: 145行

return command(name, **attrs)

​ —->decorators.py: 108行

def command(name=None, cls=None, **attrs):

​ —->decorators.py: 136行

return decorator

—->group(cli)

—->decorators.py: 132行

cmd = _make_command(f, name, attrs, cls)

​ —->decorators.py: 100-105行

cls为Group,实例化Group对象

return cls(
    name=name or f.__name__.lower().replace("_", "-"),
    callback=f,
    params=params,
    **attrs
)

​ —->core.py:1340行

MultiCommand.__init__(self, name, **attrs)

​ —->core.py: 1107行

Command.__init__(self, name, **attrs)

​ —->core.py: 881行

BaseCommand.__init__(self, name, context_settings)

—-> decorators.py: 134行

cmd值为Group类实例

return cmd   #<Group cli>

4)

cli.add_command(prepare)

—->decorators.py: 1344行

def add_command(self, cmd, name=None):

—->decorators.py: 1351行

_check_multicommand(self, name, cmd, register=True)

主要作用是向cli.commands添加prepare实例

5)

if name == ‘main’:

​ cli()

—->core.py: 828行

实例cli直接调用执行BaseCommand类的__call__方法

def __call__(self, *args, **kwargs):

—->core.py: 830行

return self.main(*args, **kwargs)

​ —->core.py: 716行

def main(

​ —->core.py:766行

得到用户输入的命令行参数

args = get_os_args()

—->core.py: 782行

保存上下文环境ctx

with self.make_context(prog_name, args, **extra) as ctx:

—->core.py: 783行

rv = self.invoke(ctx)

​ —->core.py: 1260行

return _process_result(sub_ctx.command.invoke(sub_ctx))

​ —->core.py: 1067行

return ctx.invoke(self.callback, **ctx.params)

​ —->core.py:610行

callback为prepare

return callback(*args, **kwargs)

—->prepare.py: 35行

def prepare(conf, with_notary, with_clair, with_trivy, with_chartmuseum):

至此进入prepare函数中,根据命令行参数和预配置执行创建文件,最后合成一个docker-compose.yml配置文件

这里一样,单独分析其中一个redis组件配置过程,将其他的相同类型的注释掉

delfile(config_dir)
config_dict = parse_yaml_config(conf, with_notary=with_notary, with_clair=with_clair, with_trivy=with_trivy, with_chartmuseum=with_chartmuseum)
try:
    validate(config_dict, notary_mode=with_notary)
except Exception as e:
    click.echo('Error happened in config validation...')
    logging.error(e)
    sys.exit(-1)

#prepare_log_configs(config_dict)
#prepare_nginx(config_dict)
#prepare_core(config_dict, with_notary=with_notary, with_clair=with_clair, #with_trivy=with_trivy, with_chartmuseum=with_chartmuseum)
#prepare_registry(config_dict)
#prepare_registry_ctl(config_dict)
#prepare_db(config_dict)
#prepare_job_service(config_dict)
prepare_redis(config_dict)
#prepare_tls(config_dict)
#prepare_trust_ca(config_dict)

#get_secret_key(secret_key_dir)

#  If Customized cert enabled
#prepare_registry_ca(
#    private_key_pem_path=private_key_pem_path,
#    root_crt_path=root_crt_path,
#    old_private_key_pem_path=old_private_key_pem_path,
#    old_crt_path=old_crt_path)

#if with_notary:
#    prepare_notary(config_dict, nginx_confd_dir, SSL_CERT_PATH, SSL_CERT_KEY_PATH)

#if with_clair:
#    prepare_clair(config_dict)
#    prepare_clair_adapter(config_dict)

#if with_trivy:
#    prepare_trivy_adapter(config_dict)

#if with_chartmuseum:
#    prepare_chartmuseum(config_dict)

prepare_docker_compose(config_dict, with_clair, with_trivy, with_notary, with_chartmuseum)

—-> prepare.py: 38行

这里conf为harbor.yml

 config_dict = parse_yaml_config(conf, with_notary=with_notary, with_clair=with_clair, with_trivy=with_trivy, with_chartmuseum=with_chartmuseum)

—->configs.py: 99行

分析harbor.yml文件,最终返回一个dict类型的变量config_dict,里面包含预定义的参数和从yml文件匹配的的参数,比较简单,我这里直接输出config_dict最终结果

def parse_yaml_config(config_file_path, with_notary, with_clair, with_trivy, with_chartmuseum):

这里由于启动脚本的时候没有附带–with-notary、–with-clair、–with-trivy、–with-chartmuseum参数所以根据click定义,这四个参数在命令行中没有给出,都为False

config_dict = { ‘registry_url’: ‘http://registry:5000’, ‘registry_controller_url’: ‘http://registryctl:8080’, ‘core_url’: ‘http://core:8080’, ‘core_local_url’: ‘http://127.0.0.1:8080’, ‘token_service_url’: ‘http://core:8080/service/token’, ‘jobservice_url’: ‘http://jobservice:8080’, ‘clair_url’: ‘http://clair:6060’, ‘clair_adapter_url’: ‘http://clair-adapter:8080’, ‘trivy_adapter_url’: ‘http://trivy-adapter:8080’, ‘notary_url’: ‘http://notary-server:4443’, ‘chart_repository_url’: ‘http://chartmuseum:9999’, ‘public_url’:’http://192.168.140.210’, ‘harbor_db_host:’postgresql’, ‘harbor_db_port’:5432, ‘harbor_db_name’:’registry’, ‘harbor_db_username’:’postgres’, ‘harbor_db_password’:’root123’, ‘harbor_db_sslmode’:’disable’ , ‘harbor_db_max_idle_conns’:50, ‘harbor_db_max_open_conns’:1000, ‘harbor_db_host:’postgresql’, ‘data_volume’:’/data’, ‘harbor_admin_password’:’Harbor12345’, ‘registry_custom_ca_bundle_path’:’’, ‘core_http_proxy’:’’, ‘core_https_proxy’:’’, ‘clair_db’:’postgres’, ‘clair_updaters_interval’:12, ‘trivy_github_token’:’’, ‘trivy_skip_update’:False, ‘trivy_skip_update’:False, ‘trivy_skip_update’:False, ‘trivy_skip_update’:False, ‘trivy_skip_update’:10, ‘jobservice_secret’:jsde24f3w4df342n, ‘webhook_job_max_retry’:10, ‘log_level’:’info’, ‘log_location’:’/var/log/harbor’, ‘log_rotate_count’:50, ‘log_rotate_size’:’200M’, ‘log_external’:False, ‘external_database’:False, ‘redis_host’:’redis’, ‘redis_port’:6379, ‘redis_password’:’’, ‘redis_db_index_reg’:1, ‘redis_db_index_js’:2, ‘redis_db_index_chart’:3, ‘redis_db_index_chart’:30, ‘redis_url_js’:’redis://redis:6379/2’, ‘redis_url_reg’:’redis://redis:6379/1’, ‘core_secret’:’ddwwe243d2de3d23’, ‘uaa’:’’, ‘registry_username’:’harbor_registry_user’, ‘registry_password’:’y4h7dd6ff3lmn23bh7g4vvd349jgd3s4’, ‘internal_tls’:’InternalTLS()’

​ }

jobservice_secret、core_secret、registry_password是通过secrets模块得到的16位和32位随机生成数,internal_tls.enabled值为False,然后其中关于redis参数是通过下面这个332行代码得到的

config_dict.update(get_redis_configs(configs.get("external_redis", None), with_clair, with_trivy))

这个函数得到的内容为dict,值更新到上面的config_dict中

configs={

‘redis_host’:’redis’, ‘redis_port’:6379, ‘redis_password’:’’, ‘redis_db_index_reg’:1, ‘redis_db_index_js’:2, ‘redis_db_index_chart’:3, ‘redis_db_index_chart’:30, ‘redis_url_js’:’redis://redis:6379/2’, ‘redis_url_reg’:’redis://redis:6379/1’,

}

—>prepare.py: 40行

验证config_dict生成的键值对是否存在错误值,如存在错误值根据相应raise进行错误警告

 validate(config_dict, notary_mode=with_notary)

—->prepare.py: 53行

prepare_redis(config_dict)

​ —->redis.py: 8行

def prepare_redis(config_dict):

​ —->misc.py: 81行

创建redis目录(在prepare容器中执行),并赋值权限,根据uid,gid更改所有者 uid,gid值从/utils/g.py中获取

def prepare_dir(root: str, *args, **kwargs) -> str:

—-> prepare.py: 79行

最终目的是生成docker-compose.yml文件使用docker-compose部署harbor容器
模板文件:docker-compose.yml.jinja
生成yml:/compose_location/docker-compose.yml

prepare_docker_compose(config_dict, with_clair, with_trivy, with_notary, with_chartmuseum)

​ —->docker_compose.py: 11行
config为之前生成的config_dict

def prepare_docker_compose(configs, with_clair, with_trivy, with_notary, with_chartmuseum):

​ —->docker_compose.py: 15-34行
VERSION_TAG值为versions文件中VERSION_TAG的值,configs对应的值可以在config_dict找到

 15     rendering_variables = {
 16         'version': VERSION_TAG,
 17         'reg_version': VERSION_TAG,
 18         'redis_version': VERSION_TAG,
 19         'notary_version': VERSION_TAG,
 20         'clair_version': VERSION_TAG,
 21         'clair_adapter_version': VERSION_TAG,
 22         'trivy_adapter_version': VERSION_TAG,
 23         'chartmuseum_version': VERSION_TAG,
 24         'data_volume': configs['data_volume'],
 25         'log_location': configs['log_location'],
 26         'protocol': configs['protocol'],
 27         'http_port': configs['http_port'],
 28         'external_redis': configs['external_redis'],
 29         'external_database': configs['external_database'],
 30         'with_notary': with_notary,
 31         'with_clair': with_clair,
 32         'with_trivy': with_trivy,
 33         'with_chartmuseum': with_chartmuseum
 34     }

​ —->docker_compose.py: 64行

 render_jinja(docker_compose_template_path, docker_compose_yml_path,  mode=0o644, **rendering_variables)

​ —->jinja.py: 6行

def render_jinja(src, dest,mode=0o640, uid=0, gid=0, **kw):

​ —->jinja.py: 9行
kw为rendering_variables

f.write(t.render(**kw))

​ —->misc.py: 10行
更改文件权限,设置uid和gid

def mark_file(path, mode=0o600, uid=DEFAULT_UID, gid=DEFAULT_GID):

至此docker-compose.yml文件生成完毕

声明:本博客的原创文章,都是本人平时学习所做的笔记,转载请标注出处,谢谢合作。