Metaprogramming: a robot helps you writing code

Tags metaprogramming


2021-03-14 22:01:54


What is Metaprogramming

Metaprogramming, for short, is a program that can treat other programs as data. It can be designed to read, generate, analyze or transform other programs. Detail

In this article, we concentrate on static code not in the process running phase.

Macro

Macro means how a code block should be mapped to a replacement output. Detail. Macro is a commonly used tool for the implementation of MetaProgramming.

Macro in C

Suppose you are new to the C language. You may don’t know you are already use Metaprogramming in action. Such as the following example:

some examples:

e.g 1:

#include <stdio.h>
int main() {
   printf("Hello, World!");
   return 0;
}

Here we got #include is a macro to import function definition from the header file.

e.g 2:

#define PI 3.14159

This code fragment will cause the string “PI” to be replaced with “3.14159”, we call it parameterized macro.

e.g 3:

#if WIN32
	#include	<winsock.h>
#elif LINUX
	#include	<sys/socket.h>
#endif

A macro could read variables. Different target platforms use specific herder files.

e.g 4:

#define pred(x)  ((x)-1)

We can easily take advantage of the inline function definition through the definition of a Macro. We don’t care about the type of x cause the code needs to expand before compile. Moreover, define Macro as a function without any type definition. It can provide you lots of flexibility to make your project more clean and readable.

the C language toolchain compiles your source code through the following workflow in detail:

summary

As the previous example showing that Metaprogramming is not a strange thing. We often use it in daily programming.

Code replace in action

AST parser

For actually and safety code replace, we need to study two new concepts: AST and AST parser.

AST is the abbreviation of Abstract Syntax Tree, a kind of tree data structure. Each node of the tree denotes a construct occurring in the source code. Detail

AST parser is a library or package for parsing a source code file or fragment to a tree data structure tool. Programming language specification too complex to replace by simple string replacement. It can not cover most usage cases. We use the AST parser tool to search or operation a tree structure to meet the requirements.

example parser for jsx

babelParser = require('@babel/parser');
const res = babelParser.parse(`
ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);`,{plugins:[ 'jsx', 'flow' ]});
console.log(JSON.stringify(res));

Then we got a very large JSON to tell us every detail of this code. pastebin: detail

{
  "type": "CallExpression",
  "start": 1,
  "end": 79,
  "loc": {
    "start": {
      "line": 2,
      "column": 0
    },
    "end": {
      "line": 5,
      "column": 1
    }
  },
  "callee": {
    "blah":"blah"
  }
}

Here’s part of the result. As an example, we can get the information that the expression is call function expression and get the code’s position. Even the location line and column information and tell us every callee (every member of function call parameter)

The parser is a part of the language compiler (or part of the compiler frontend). The parser is often used as an infrastructure for code static analysis and code automation complete in an editor.

More example:

  1. Markdown-parser https://www.npmjs.com/package/markdown-parser
  2. yaml-parser https://www.npmjs.com/package/js-yaml

code scanning for ci

After parsing the source code to the syntax tree, we can write a deep search first algorithm for specific coding behavior. Then deploy the algorithm to CI workflow before compile source code.

Here’s an example for checkout whether any code behavior returns an error but doesn’t return the None 200 status code. The function implementation based on if we check error is not nil there must call some function w.Writeheader(xxx), and the parameter may not be 2xx

We got this requirement from the middleware team, who develop a web load balance and monitor the website’s error status.

For shorter and straightforward we made a small example only check a function is HTTP handler:

package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
)

func main() {
	// src is the input for which we want to print the AST.
	src := `
package main
func (rt *Runtime) FileReceive(w http.ResponseWriter, r *http.Request) {
}
`

	// Create the AST by parsing src.
	fset := token.NewFileSet() // positions are relative to fset
	f, err := parser.ParseFile(fset, "", src, 0)
	if err != nil {
		panic(err)
	}

  // Print the AST.
  ast.Print(fset, f)

	ast.Inspect(f, func(n ast.Node) bool { // deep first node visitor
		switch x := n.(type) {
		case *ast.FuncType: // check node is a function
			if len(x.Params.List) != 2 { // check function parameter length
				return false
			}
			p1, p2 := false, false
			if val, ok := x.Params.List[0].Type.(*ast.SelectorExpr); ok { // check parameter type is http.ResponseWriter
				if valExpr, ok := val.X.(*ast.Ident); ok {
					if valExpr.Name == "http" && val.Sel.Name == "ResponseWriter" {
						p1 = true
					}
				}
			}
			if val, ok := x.Params.List[1].Type.(*ast.StarExpr); ok { // check parameter type is *http.Request
				if starX, ok := val.X.(*ast.SelectorExpr); ok {
					if starXX, ok := starX.X.(*ast.Ident); ok {
						if starXX.Name == "http" && starX.Sel.Name == "Request" {
							p2 = true
						}
					}
				}
			}
			if p1 && p2 {
				fmt.Println("handler found")
			}
		}
		return true
	})
}

Tips: type assertation following the printed AST node tree.

example of replacing code

Follow the path got safe and accurately replace code result:

  1. reuse previously mentioned search algorithm methodology
  2. direct operation to the syntax tree
  3. compile the syntax tree to source code
  4. format code makes it looks pretty cool.

Use case: When you got a GO project to upgrade the original log module (fmt.Println) to a newly designed log module (log.Log())

and example code for golang (by golang.org/x/tools/go/ast/astutil) https://play.golang.org/p/jDSpmV_Kxnt

example of code gen

For the example of code generation, we have already have lots of widely used tools:

protobuf

GRPC is a common tool for RPC. The message encoding uses the standard named protobuf.

here is an example for code generation use following protobuf file example fill the left blank: https://protogen.marcgravell.com/

example protobuf file:

syntax = "proto3";

message ExampleMessage {
    int32 foo = 1;
    string bar = 3;
}

sqlc

To fully control the SQL query and performance, we don’t use ORM but code generation. This code generation tool can help us a lot for those repeat work on translate SQL results to the local data structure.

Example convert SQL file to golang DAO layer: https://play.sqlc.dev/

write your own robot (code generator)

In typical code snippet are high-frequency use cases to generate code fragments.

A snippet is a valuable tool under the editor. Using snippet:

Pros:

  1. easy to define
  2. simple template support

Cons:

  1. Can not generate code relies on logic or complex data.

In the contrast code generation:

Pros:

  1. could generate code through a complex data structure like JSON or SQL file
  2. more quickly working with CI for upgrade generate the result. That means if the generated code needs an update, we upgrade the code generator and regenerate code from the data source. But snippets must be working with the legacy generate result.
  3. Better for a large project

Cons:

  1. overhead integrate with your project
  2. a little hard to write code and test what you write.

We can write our code generator with lots of tools like https://github.com/dave/jennifer

Tip: When you write a code generator, think about how you orchestrate tokens of your familiar language.

In summary

Metaprogramming is a commonly used method for programming. It can reduce repeat work and improve engineering quality.

Learning, sharing, and improve together.


本人博客文章采用CC Attribution-NonCommercial协议: CC Attribution-NonCommercial 必须保留原作者署名,并且不允许用于商业用途,其他行为都是允许的。

deploy docker registry under private network

Tags docker registry ssl nginx


2021-02-15 15:30:35


Docker registry is an essential infrastructure of docker daemon or Kubernetes. We package project artifacts by docker image while storage and distribution by registry service. Today we will show you how we are setting up a straightforward and small registry implementation by docker official. It convenience a docking workflow for CI/CD.

Deploy structure:

image

Deploy services

Container

docker run -d -p 5000:5000 --restart=always --name registry -v /data/registry:/var/lib/registry registry:2
docker run -d -p 5001:80 --name registry-ui -e DELETE_IMAGES=true joxit/docker-registry-ui:static
  1. Replace path /data/registry to your own storage path
  2. -e DELETE_IMAGES=true intends docker images can delete through UI operation
    1. Reference document by link https://hub.docker.com/r/joxit/docker-registry-ui
    2. In paragraph Run the static interface
  3. Code review image joxit/docker-registry-ui:static docker file we can know:
    1. The HTTP service is just an nginx process with a bunch of static HTTP static files.
    2. The cross region and registry_url and SSL config can be moved to our nginx deploy for more flexible and clean config management.

Host nginx deploy

Generally, we install nginx by Linux package management such as apt.

We can install nginx under ubuntu by the command sudo apt-get update && sudo apt-get install -y nginx

Then we install the following config file under your config dir. The default path is /etc/nginx/sites-enabled/

Here we storage the config file in /etc/nginx/sites-enabled/registry

server {
    listen 443 ssl;
    server_name [[REPLACE: YOUR OWN DOMAIN NAME]];
    ssl_certificate     /etc/ssl/[[REPLACE: YOUR DOMAIN SSL CRT FILE]];
    ssl_certificate_key /etc/ssl/[[REPLACE: YOUR DOMAIN SSL KEY FILE]];
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    client_max_body_size 2048M;
    location / {
        proxy_pass http://127.0.0.1:5001;
    }
    location /v2 {
        proxy_pass http://127.0.0.1:5000;
    }
}

server{
    listen 80;
    server_name [[REPLACE: YOUR OWN DOMAIN NAME]];
    return 301 https://$host$request_uri;
}

ATTENTION: please replace the config with your environment situation.

  1. The parameter server_name must replace with your domain name.
  2. We recommend using Let’s encrypt DNS-01 challenge to verify your domain and get an SSL cert file.
  3. The parameter ssl_certificate must replace with your domain crt file.
  4. The parameter ssl_certificate_key must replace with your domain key file.
  5. The parameter client_max_body_size at 2GB since we usually push a large docker image layer in practice.
  6. location / route to registry UI container.
  7. location /v2 route to registry service.
  8. Don’t forget to set A record for your domain.
  9. We highly recommend setting up nginx HTTPS for your service since the docker daemon or kubelet needs other configs to trust your registry.
  10. The second server under the config file which helps us force switch from HTTP to HTTPS

SAFETY WARNING:

  1. Do not deploy this solution in the public network.
  2. Use it in a small team under a private network.

本人博客文章采用CC Attribution-NonCommercial协议: CC Attribution-NonCommercial 必须保留原作者署名,并且不允许用于商业用途,其他行为都是允许的。

my vim golang programming environment

Tags golang vim docker


2021-02-14 23:35:19


If you are also suffered from vim emulation or facked vim such as ideavim or vscodevim. Today we will show you a new golang development environment under docker or use it out of the box. And easy to tweak by our public dockerfile and built image from docker hub public image. Here’s a list of features in the following article.

features

Autocomplete by language server without import package we need. While showing function parameters and documents.

image

We use onedark color theme as the basic vim color solution. Here we give two everyday operations examples as below.

  1. Rename variable
  2. Add tags to struct

asciicast

The example below use ETCD as demo project show the following feature:

  1. tini for container init process
  2. auto complete
  3. go to definition
  4. FZF search file

asciicast

Of course gopls (which is a golang toolchain we depend on) use over 40 seconds to scan so large project. But gopls also have a cache feature accelerate open project in the next time.

how to use it

pull the docker image by command:

docker pull lijianying10/golangdev:21Feb7-01

docker run by following command

docker run -it --rm -v $PWD/etcd:/root/etcd lijianying10/golangdev:21Feb7-01 /bin/bash

Attention: alter the dir mapping to your project path, and we highly recommend using gomod as the project manager.

Dockerfile

ref link: https://github.com/lijianying10/FixLinux/blob/master/golangdev/Dockerfile

vim dot file

ref link: https://github.com/lijianying10/FixLinux/blob/master/dotfile/.vimrc

shortcut keys (key maps)

start from LOC 79 of dot file.

nmap <M-p> :TagbarToggle<CR> " view tag bar
imap <M-p> <esc>:TagbarToggle<CR>i
nmap <M-u> :NERDTreeToggle<CR> " view file list
imap <M-u> <esc>:NERDTreeToggle<CR>
nmap <C-c> :q<CR> " exit 
nmap <M-o> :tabn<CR> " tab next
imap <M-o> <esc>:tabn<CR>
nmap <M-i> :tabp<CR> " tab previous
imap <M-i> <esc>:tabp<CR>
nmap <M-l> :w<CR>:GoMetaLinter<CR> " linter 
nmap <M-n> <Plug>(coc-definition) " go to definition
nmap <C-z> :undo<CR> " undo
nmap <M-y> :GoErrCheck<CR> " go error check
nmap <C-s> :w<CR> " save
imap <C-s> <esc>:w<CR>
imap <M-c> <esc>:pc<CR>
nmap <M-c> :pc<CR> " close preview window
nmap <leader>r :Ack<space> " search hole project document: https://github.com/mileszs/ack.vim
nmap <leader>t :FZF<CR> " zfz file search

example key mapping:

  1. M-p means Meta + p Option key for mac and alt key for windows keyboard
  2. C-s means Ctrl + s
  3. <leader>t means press \ and then press t

本人博客文章采用CC Attribution-NonCommercial协议: CC Attribution-NonCommercial 必须保留原作者署名,并且不允许用于商业用途,其他行为都是允许的。

bazel macro 的开发

Tags bazel


2021-02-06 13:17:41


当你有了很多rules可以用在你的工程,当你做一些事务去构建你的目标你会发现BUILDfile开始出现大段的只有参数不同的重复代码降低了代码的复用率。

bazel 的不同于其他构建过程的配置并不是创造一个DSL出来解决这个问题。而通过 Starlark 语言来做到 target 分析。 其关键点在于bazel的运行分为三个阶段 (three phase) 其中我们使用 starlark 语言去定义我们的构建目标的过程是在第二个阶段。

一个Macro的例子

load(":render.bzl", "blender_render")
load("//ffmpeg:ffmpeg.bzl", "ffmpeg_combine_video")

def blender_render_batch(targets, out):
    render_targets = []
    for t in targets:
        lb = Label(t)
        render_targets.append(t+"_render")
        blender_render(
            name = lb.name+"_render",
            blender_project = t,
            out = lb.name+".mp4"
        )
    ffmpeg_combine_video(
        name = out+"_render",
        input = render_targets,
        out = out+".mp4",
    )

CodeReview提示:进入函数之后我们通过 blender_render 这个 rule 把数组内标记的 blender project 渲染成 mp4 视频文件并且记录相关的渲染过程的 target 到数组 render_targets 中最后放到 ffmpeg_combine_video 这个 rule 把这一系列的mp4文件合并成为一个整体视频。这样我们就可以通过这个方法实现视频的工业化生产减少人工剪辑的介入。

让我们来观察上面的macro实现

  1. 我们需要把上面的代码放到 render_batch.bzl 中。
    1. 然后我们可以通过 load("@rules_3dmodule//blender:render_batch.bzl","blender_render_batch") 如上的load代码就可以准备好调用上面的函数实现了。
  2. 我们可以看到其实还是与写 rule 很像,只是我们并不需要定义 rule 函数中也不需要有 ctx 更不需要 actions 有所执行。
  3. 其中核心的功能实现是通过 load rule 之后把 rule 当成函数调用即可。
  4. 在写 starlark 代码中需要注意只有数组(array)和字典(dict)是可以修改(mutable)的变量其他的都是不可修改(immutable)变量这是为了并行, 这在代码开发中至关重要。按照普通开发语言的开发思路去写会因此掉到坑里。

深入解析

通过上面的例子我们可以通过分析了解到: Analysis phase 从原理上来讲是通过 (ctx.action)[https://docs.bazel.build/versions/master/skylark/lib/actions.html] 作为 (DAG)[https://en.wikipedia.org/wiki/Directed_acyclic_graph]Node 我们可以从这里 来印证我们的观察。 因此我们写的代码或者准确来说 Macro 是通过运行之后帮助 bazel 来构建整个 DAG 的过程。 掌握了这个核心思想之后再开发 Macro 会轻松很多。

状态处理

当我们学会了把一些列固定的操作写到函数当中很快你会发现你需要对状态进行处理。 例如在我的工程中,我需要对每个 blender target 标记需要并且让构建运行时知道自己在整体工程中的位置,从视频剪辑的角度来讲叫做场序(或者说你是第几个视频片段)因此我们写的Macro需要处理运行状态问题。

一个例子:

以下为文件 counter.bzl 的内容

def video_scene_append(target_list,target):
    c = len(target_list)
    target_list.append("//%s:%s"%(native.package_name(),target))
    return c

文件说明: 我们把target变量append到target_list当中,并且返回target在数组中的index作为函数返回。 这样我们就可以定义出target在构建中的序号了。

以下为 BUILD 的文件内容

load("counter.bzl","video_scene_append")

# 1. storage video sequence list
# 2. as a counter for video move sequence
video_scene_list = []

video_scene_append(video_scene_list,"stag1")

我们把状态存储的变量放到 BUILD 文件当中, 并且调用函数实现功能。

配置处理建议

我们可以使用 Jsonnet 来作为配置生成的入口下面是一个例子:

BUILD file 当中

jsonnet_to_json(
    name = "config_gen_stag4", 
    src = "databargroup_config.jsonnet", 
    outs = ["config_gen_stag4.json"], 
    ext_code = {
        "config": """
        {
            default_shift_between_bar:2.6,
            animation_config+:{
                data_bar_keep_frames:24*2,
            },
            num_panel_cfg+:{
                data_division:1.0
            }
        }
        """,
        "data_bar_count":str(first_video_consts_get(stag4_dbg_count_key_name)),
    }, 
)

databargroup_config.jsonnet 当中

local tmpl = {
  "default_shift_between_bar": 1.3,
  "title_panel_size_x": 2,
  "title_panel_size_y": 1,
  "title_panel_scale": 0.9,
  "title_panel_distance_to_data_bar": -1.2,
  "title_panel_distance_to_camera": 0.4,
  "num_panel_exist": true,
  "num_panel_cfg": {
    "blah": "blah",
  },
  "animation_config": {
    "blah": "blah",,
  }
};

tmpl + std.extVar('config') + {data_bar_count:std.extVar('data_bar_count')} 

这样我们在真正执行构建 stag4 这个视频片段就可以使用 config_gen_stag4 渲染好的 json 来执行 3d 建模生产 blender project

其中 Jsonnet 语法可以参考关于 OOP 的语法解释。

配置硬编码问题

当你解决了配置生成问题之后会遇到配置硬编码的问题这里没什么好分享的可以直接参考文档

具体使用中可以参考 .bazelrc 的说明。

从上面的参考文档我们可以知道 flag 的名字是可以使用 bazel label 作为名字的因此我们的 bazelrc 还是一种代码生成很友好的解决方案。

最后

我经过了上面的学习和实践同时我也学习到了 starlark-gogo-jsonnet 结合在一起会是一个非常好的组合, 无论是在配置,编排,还是自动化领域都是一个不错工具。 在未来的产品开发和构建当中我应该会使用这种组合来提升我的人效。


本人博客文章采用CC Attribution-NonCommercial协议: CC Attribution-NonCommercial 必须保留原作者署名,并且不允许用于商业用途,其他行为都是允许的。

bazel 使用经验和记录

Tags bazel


2021-01-15 19:01:18


近期我自己做的项目已经用上 Bazel 这款产品了。 它是我目前学过的最难学的一门技术(工具)了。 下面总结一下我的学习和使用过程和方法。 一方面我想提升自己的总结能力,另外一方面我自己学习的时候很寂寞身边没有人会这门技术,大多都是用一用编译就好了, 并不想深入了解。

我为什么要用bazel

我最近在做3d建模视频我的主要业务逻辑使用的go语言,blender只能使用python语言,不少操作的驱动需要使用python实现。 不少SVG从矢量到标量的计算用到了NodeJS,并且我的项目并不能统一语言去做。

依赖复杂,我有很多种action而且未来还会有更多。例如目前我有的操作:

  1. SVG矢量转标量
  2. 依赖上面的结果让两个svg path 正交 绘制 mesh 并且输出 Wavefront OBJ 3d 模型的格式 (未来还会有更多的 Mesh 建模算法来应对各种不同的建模场景)
  3. 运行blender background 模式调用我写的 python library 和 python RPC server 对接 golang 建模用业务层。
  4. blender建模和动画的结果之间可以相互引用和复用。

所以这是一个相当复杂的 Dag (有向无环图) 因此每当我动一个比较底层的算法我需要 快速的 正确的 构建并且执行Dag依赖树。

当我编译好我的blender工程之后我就可以把blender文件上传到GPU集群来渲染视频。

一个更简单的为什么要使用bazel的例子:

当你自己写了一个 golang 程序帮助你输入一个 SQL 语句输出一个对应的驱动包来避免使用 ORM 当你修改了 code gen 的模板时你怎么知道该更新那些和测试那些依赖于它的目标 ( Target ) 呢?

并且你并不想写一大堆重复的类似的代码 干体力活 希望机器生成代码, 并且你知道 Golang 没有泛型造成很多重复代码时, 我们唯一的选择就只能是设计一个Code Generator。

学习路线

这里记录一下我的学习路线方便当我自己忘记知识点可以看这篇文字帮助自己复习, 我的学习目标并不是 使用 bazel 而是熟练的掌握开发 rules 应对各类情况。

Concepts 了解基本概念全部读完。

举个例子:

Transitive dependencies
Bazel only reads dependencies listed in your WORKSPACE file. If your project (A) depends on another project (B) which lists a dependency on a third project (C) in its WORKSPACE file, youll have to add both B and C to your projects WORKSPACE file. This requirement can balloon the WORKSPACE file size, but limits the chances of having one library include C at version 1.0 and another include C at 2.0.

当你的 Workspace 越来越大时如果你能想起这里来。 你会发现并不是忘记读了什么文档使用什么方法而是就这样设计的。

Concepts 里面有非常多设计者的意图,是一个系统设计非常好的学习资料。

Extending Bazel 中的 Extension Overview 和 Concepts

Extending Bazel 其实就是写rules来处理自己的工作情况。 这里一定都要读完,意义很大比如一个重要的概念 Depsets 你读完文档之后并不知道它是 immutable variable。 如果你并不理解文档的情况下就会一脸懵逼的碰到 rules 的设计问题,很多变量是只读的文档里面没有提到但是你要反应过来。 所以前期文档阅读量不足会耽误很多时间。因为遇到问题你反应不过来。

换个角度继续学

当我看完上面的文档时还不知道怎么下手, 云里雾里, 这时候有一个重要的 blog 出现: https://www.jayconrod.com/posts/106/writing-bazel-rules--simple-binary-rule

这些文章需要拿出来反复琢磨:

  1. Writing Bazel rules: simple binary rule 帮助你学习如何构建好一个target
  2. Writing Bazel rules: library rule, depsets, providers 帮助你学习如何处理好依赖,很重要,因为你知道bazel的核心依赖算法是DAG
  3. Writing Bazel rules: repository rules 帮助你把外部的软件转换成一个 bazel 系统可以接受的依赖很重要,比如说你构建时依赖了 blender 的二进制文件去运行, 它就会帮助你做到类似于 rules_go 一样下载 golang 的 toolchain

以上是写的非常好的非官方的tutorial。

我看到这时还是不会写。接下来开始读源码推荐阅读:

  1. 一个复杂的: GITHUB REPO 去看首页文档写的重要API的实现。
  2. 一个简单的: GITHUB REPO 去看比较简单的case的关键rules实现。

当我看到这里时我基本上已经写了个大概。因为已经大概熟悉了 Skylark 这门语言。进入爬坑阶段。

最重要的我常常需要参考的关键数据结构

Actions https://docs.bazel.build/versions/master/skylark/lib/actions.html 这是核心中的核心 Files https://docs.bazel.build/versions/master/skylark/lib/File.html 写 rules 其实就是灵活的处理你的代码文件与编译器之间做沟通。 DefaultInfo https://docs.bazel.build/versions/master/skylark/lib/DefaultInfo.html 理解好这个概念可以帮助你与其他开发者写的 Rules 很好的做对接和依赖。

爬坑经验

代理方案选择

  1. linux iptables ip filter 省事
  2. windows Proxifier

windows 注意:

因为 Proxifier 是按照进程为最基本单位的, 当你看不到具体那个进程网络卡住了可以使用工具,可以看到进程树:

process monitor: https://docs.microsoft.com/en-us/sysinternals/downloads/procmon

目前我的进程rules: bazel.exe;java.exe;fetch_repo.exe;Conhost.exe;go.exe;git.exe;git-remote-https.exe; 供参考

管理你系统外的依赖

系统外依赖的定义是: 你并不关心实现的代码。仅仅是下载, 比如说 GJSON 这类的

Cons:

你在A项目中引用了B项目 B项目引用了GJSON 那么A的workspace 也需要管理 GJSON 依赖。 这时你的 WORKSPACE 文件会像气球一样膨胀 我遇到这个坑是因为我对 Concept 文档理解的不够深入。

Pros:

可以实现 Shadowing dependencies 在文档: https://docs.bazel.build/versions/master/external.html 提到的

关于 actions run 这个函数的坑

这些坑都是希望假如我身边有人会这技术我希望他能提前告诉我的

  1. declare_file 一定要放到output里面。 这样当你的 actions.run 结束时文件没创建, 会显示编译错误。
  2. inputs 一定要把你依赖的所有文件都放到这个数组里面。 就算 bazel query 时的确显示出了正确的依赖关系, 但是 inputs 没有声明这些文件在actions中依赖了你会你修改了文件不会重新编译。 会破坏编译的正确性。
  3. mnemonic 只能是一个单词,放入要给动词可以帮助你理解正在并行编译时在做什么动作。
  4. executable 不要依赖与你的path或者一个绝对路径那就出现了环境依赖,要使用 target 来保证整个系统时封闭的来保证你的 正确性
  5. use_default_shell_env False = 有节操 True = 没节操 如果你选择省事至少放到Docker容器里面定义好Path运行。不然会出现环境依赖以及脱离 bazel 环境管理的软件依赖。
  6. tools 假如你在构建中依赖了其他的二进制文件, 例如我在运行 blender 我的入口时 python 的 RPC Server, 我用 Golang 写 RPC client 你很熟练的通过 arguments 或者 env 把你的 Golang client 二进制文件放到了 执行的地方,但是你会新奇的发现 bazel 系统的构建 DAG 树的 Graphviz 图纸是有依赖的但是它并不构建, 然而你读了很多遍文档也不知道为什么你依赖的二进制 bazel target 不编译因为文档的描述是这样的: tools: List or depset of any tools needed by the action. Tools are inputs with additional runfiles that are automatically made available to the action. 现在我可以高兴的告诉你这时候要放到tools里面。出现这个问题并不是文档有问题,是 bazel 的设计者经验丰富抽象层次高造成的。但是我给跪了。

input 数组的开发技巧

例如说你依赖了 py_library 这个 bazel native rule

第一你需要debug看这个rules返回给你什么info

下面时rules label attr

"util":attr.label(),

输入参数label例子: 下面这个例子的util其实就是一个 py_library rule

util = "//blenderutil:util",

下面时skylark 调试代码, 可以看到有什么Info

print(ctx.attr.util)

一点点的调试最后可以得到下面的代码:

for f in ctx.attr.util[PyInfo].transitive_sources.to_list():
    input_list.append(f)

你会得到你所有依赖的内容如果你的依赖很复杂 比如说 A->B->C A->D->C 你会发现C重复了。可以直接用depset解决这个问题 new_var = depset([这里时你的数组,或者直接把变量放进去])

executable 的例子

定义label

"_blenderRunner":attr.label(
    default = Label("//blenderRunner:blenderRunner"),
    executable = True,
    cfg = "exec"
),

action.run 参数例子

executable = ctx.executable._blenderRunner, 

一些感悟

为什么 golang 要用 bazel

目前 gomod 已经做的够好了,就算在我的这个工程里面也会使用 golang gomod 来开发和测试我的代码。 但是与 bazel 结合其实成本并不高有重要的工具叫 gazelle 他可以帮助你转换 gomod 项目成 bazel 项目。 也可以帮助你同步 gomod 到 bazel。

通过如下命令:

bazel run //:gazelle # 初始化 
bazel run //:gazelle -- update-repos -from_file=go.mod -to_macro=deps.bzl%go_dependencies # 更新

需要注意:

  1. 你还是要显式从 WORKSPACE 或者你自己的 bazel function 中声明你的 go_repository rule 来定义依赖。
  2. 关于 golang bazel 项目吐槽最多的 protobuf 生成之后 IDE 很难结合的问题可以结合这个思路 https://github.com/nikunjy/golink 另外我在自己读 bazel rule 开发文档的时候看到其实我们是可以通过创建软连接来帮助IDE找到我们已经生成的代码。
  3. 上面的问题还有另外一个解决思路, 修改 Gocode 的实现,目前 Gocode 这个项目已经有 daemon 了。 但是它扫描代码并不积极。

一个恰到好处的设计

从actions.run这里可以看到, 每个参数设计的都很到位, 尤其是限制的很到位, 并且每个每个参数都不能删掉, 并且很好的兼顾了各种情况。 新手只可能对概念的理解和知识的掌握不到位, 很难产生一些错误, 这大概就是牛逼的架构师设计的接口。 把安全做到语言级别, 给你报错而不是你在写代码的时候给你各种需要主动遵守的规范。

如果你并不享受这些对你的限制,还不如用 python 或者 make 之类作为构建工具。

一些 bazel 相比其他构建系统的好处

  1. 封装的好,会让构建过程产出结果稳定,受环境的影响可以做到尽量小,也可以让依赖多个版本并存
  2. Starlark 语言设计的好, 类似 python 写起来贼舒服被裁剪的很好, 一方面是影响话编译环境的API都被裁掉了(你并不能很容易的写文件到磁盘到任意位置)内部帮助你构建的变量都是只读的。当然它的好是相对于 GnuMake 和 CMake 相比。
  3. 因为拓展性好,基本上你能想到的语言都有了现成的Rules支持。
  4. 对 Code Generation 非常友好。
  5. 正确 快速 实至名归

最后

很开心我看到了 Bazel 的门, 希望我能早日入门。


本人博客文章采用CC Attribution-NonCommercial协议: CC Attribution-NonCommercial 必须保留原作者署名,并且不允许用于商业用途,其他行为都是允许的。

小型团队内网创建docker registry

Tags docker registry ssl nginx


2021-01-14 12:07:26


docker registry 是作为容器相关自动化关键底层基础设施,作为一种重要的artifact存储形式,对接CI/CD Kubernetes 都非常方便。

结构设计:


                                       ---> registry (run in docker container)
http traffic -> host nginx with ssl ---|
                                       ---> registry UI (run in docker container)


部署过程

1. 部署docker容器

docker run -d -p 5000:5000 --restart=always --name registry -v /data/registry:/var/lib/registry registry:2
docker run -d -p 5001:80 --name registry-ui -e DELETE_IMAGES=true joxit/docker-registry-ui:static

注意:

  1. regisry 存储放到了 host 的 /data/registry 位置需要可以修改
  2. ui 允许删除数据,更多选项参考:这里 的Run the static interface 段落
  3. 我快速的读了一下这个 registry-ui 的代码其实它是个静态网页项目,dockerfile 的内容显示其实里面就是个nginx 所以说明书上面的 跨域registry_urlSSL 相关配置可以直接如下的 Nginx reverse proxy配置中直接干净快速的解决。

2. 部署Nginx

$ cat /etc/nginx/sites-enabled/registry
server {
        listen 443 ssl;
        server_name hub.philo.top;
        ssl_certificate     /etc/ssl/1_hub.philo.top_bundle.crt;
        ssl_certificate_key /etc/ssl/2_hub.philo.top.key;
        ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers         HIGH:!aNULL:!MD5;
        client_max_body_size 2048M;
        location / {
                proxy_pass http://127.0.0.1:5001;
        }
        location /v2 {
                proxy_pass http://127.0.0.1:5000;
        }
}

server{
        listen 80;
        server_name hub.philo.top;
        return 301 https://$host$request_uri;
}

注意:

  1. 第一行是命令是提示文件存放位置
  2. 安装nginx的方法是 apt-get update && apt-get install -y nginx
  3. server_name 命令需要改成你自己的域名
  4. 证书淘宝买 5 块钱 店铺名字 鼎森网络科技有限公司 因为是内网使用的证书所以用Let’s encrypt比较麻烦。
  5. 命令 client_max_body_size 不要裁剪,因为docker pull 和 push 的 http body 很大。
  6. location / 的作用是路由到 ui container
  7. location /v2 的作用是路由到 docker registry
  8. 别忘了设置域名A记录
  9. SSL 证书最好是要配置上的,原因是docker pull过程默认是要证书的不然需要特别配置trust同理 kubernetes 也有类似的需求稍微衡量一下5块钱还是值得的
  10. 下面的一个Server是为了强制HTTPS

本人博客文章采用CC Attribution-NonCommercial协议: CC Attribution-NonCommercial 必须保留原作者署名,并且不允许用于商业用途,其他行为都是允许的。

网关服务器部署DNS的冲突问题解决

Tags ubuntu dns systemd-resolved


2021-01-14 10:40:11


因为5.4内核对网络的管理修改很多,在自己部署DNS服务器时,发现systemd-resolved.service占用端口53

所以这里Ubuntu 20.04 的运维SOP如下:

  1. netplan 设置nameserver 127.0.0.1 apply
  2. systemd-resolved.service stop && disable
  3. 部署你自己的dns server
  4. delete softlink /etc/resolv.conf 写入 nameserver 127.0.0.1

注意,Docker daemon会默认指定 8.8.8.8 8.8.4.4 作为dns 不会再取系统配置 (Docker 20.10.1 行为) 所以如果做了特别的DNS配置需要对Daemon配置方法参考这里 https://forums.docker.com/t/local-dns-or-public-dns-why-not-both-etc-docker-daemon-json/54544


本人博客文章采用CC Attribution-NonCommercial协议: CC Attribution-NonCommercial 必须保留原作者署名,并且不允许用于商业用途,其他行为都是允许的。

iptables 使用 Redsocks 时候 Docker 没有网络问题的分析

Tags iptables docker redsocks network


2021-01-13 20:33:10


今天我自己遇到了一个故障,Docker里面没有网络。

最开始以为是我iptables设置有错误。通过仔细阅读文档发现估计并不是iptables的配置有问题。

后来发现一个特征,凡是走Redsocks RETURN target 全都可以联通,发现这个主要是我无意间发现内网是通的。

所以定位核心故障是Redsocks不通的,这时候发现Redsocks的Listen address 是有问题的. 因为Docker是通过网桥来到local的所以IP地址不是本地所以redsocks需要监听0.0.0.0

所以正确的配置如下

chnroute 生成中国的ip地址

curl 'http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest' | grep ipv4 | grep CN | awk -F\| '{ printf("%s/%d\n", $4, 32-log($5)/log(2)) }    ' > chnroute.txt

我们默认绕过中国的ip地址

redsocks 配置文件

base {
        // debug: connection progress & client list on SIGUSR1
        log_debug = off;

        // info: start and end of client session
        log_info = on;

        /* possible `log' values are:
         *   stderr
         *   "file:/path/to/file"
         *   syslog:FACILITY  facility is any of "daemon", "local0"..."local7"
         */
        log = "syslog:daemon";

        // detach from console
        daemon = on;

        /* Change uid, gid and root directory, these options require root
         * privilegies on startup.
         * Note, your chroot may requre /etc/localtime if you write log to syslog.
         * Log is opened before chroot & uid changing.
         */
        user = redsocks;
        group = redsocks;
        // chroot = "/var/chroot";

        /* possible `redirector' values are:
         *   iptables   - for Linux
         *   ipf        - for FreeBSD
         *   pf         - for OpenBSD
         *   generic    - some generic redirector that MAY work
         */
        redirector = iptables;
}

redsocks {
        /* `local_ip' defaults to 127.0.0.1 for security reasons,
         * use 0.0.0.0 if you want to listen on every interface.
         * `local_*' are used as port to redirect to.
         */
        local_ip = 0.0.0.0;
        local_port = 12345;

        // `ip' and `port' are IP and tcp-port of proxy-server
        // You can also use hostname instead of IP, only one (random)
        // address of multihomed host will be used.
        ip = 1.2.3.4;
        port = 8089;


        // known types: socks4, socks5, http-connect, http-relay
        type = socks5;

        // login = "foobar";
        // password = "baz";
}

iptables 配置

set -x
set -e

# 下面两句是添加中国的ip表
sudo ipset create chnroute hash:net
cat chnroute.txt | sudo xargs -I ip ipset add chnroute ip

# 在 nat 表中创建新链
iptables -t nat -N REDSOCKS
## 首先这个代理要屏蔽网络代理出口服务(直接return嘛)

# 下面是添加例外端口的例子
#iptables -t nat -A REDSOCKS -p tcp --dport 59237 -j RETURN
# 下面是添加例外ip地址的例子
#iptables -t nat -A REDSOCKS -d 45.76.241.57 -j RETURN
#iptables -t nat -A REDSOCKS -d 45.32.79.211 -j RETURN
#iptables -t nat -A REDSOCKS -d 144.202.84.244 -j RETURN

# 过滤Docker container 网段 (默认的)
iptables -t nat -A REDSOCKS -d 172.17.0.0/16 -j RETURN
# 过滤局域网段
iptables -t nat -A REDSOCKS -d 10.0.0.0/8 -j RETURN
iptables -t nat -A REDSOCKS -d 127.0.0.0/8 -j RETURN
iptables -t nat -A REDSOCKS -d 169.254.0.0/16 -j RETURN
iptables -t nat -A REDSOCKS -d 172.16.0.0/12 -j RETURN
iptables -t nat -A REDSOCKS -d 192.168.0.0/16 -j RETURN
iptables -t nat -A REDSOCKS -d 224.0.0.0/4 -j RETURN
iptables -t nat -A REDSOCKS -d 240.0.0.0/4 -j RETURN
# enable 中国的ipset
iptables -t nat -A REDSOCKS -p tcp -m set --match-set chnroute dst -j RETURN
# FWD 需要走代理的情况,直接打到redsock的tcp port
iptables -t nat -A REDSOCKS -p tcp -j REDIRECT --to-ports 12345

# 让上面这个过滤用途的Chain 在PREROUTING中使用(本Linux系统作为 Gateway时候使用)
iptables -t nat -I PREROUTING -p tcp -j REDSOCKS
# 为本机的代理append chain
iptables -t nat -I OUTPUT -p tcp -j REDSOCKS
# 使用docker的时候FWD的DROP的据说是为了安全,但是这里,得给FWD了。不然转发机器无法上网
iptables -P FORWARD ACCEPT

注意事项

  1. 如果你要用本机作为gateway别忘了做这个操作: https://linuxconfig.org/how-to-turn-on-off-ip-forwarding-in-linux
  2. Redsocks 的 upstream socks5 要改成你自己的

本人博客文章采用CC Attribution-NonCommercial协议: CC Attribution-NonCommercial 必须保留原作者署名,并且不允许用于商业用途,其他行为都是允许的。