如果你忘了你的云服务器登录密码

如果你忘了云服务器的密码,有没有简单的方式找回?

有,前提是你设置了公钥登录以及可以使用 docker。

以 ubuntu 为例,只需要两步:

1
2
$ docker run -it --rm -v /etc/:/etc/ ubuntu
$ passwd your-username

然后输入你的密码就可以了。

这是为啥?因为 Docker 守护进程需要 root 权限,所以操作 docker 就可以直接使用 root 来操控系统。只要这里对系统配置相关的内容进行映射,那么在 docker 容器中就可以修改系统的任何内容,当然映射整个根目录都是可以的。

所以说公钥登录+docker 权限是很危险的。

最简单的方式使用最新开发分支的Golang编译器

使用如下的命令:

1
2
3
4
go get -v golang.org/dl/gotip
gotip download
gotip version
# go version devel +c485506 Fri Aug 16 19:54:57 2019 +0000 darwin/amd64

比如这里我就使用了 go1.13 的未发布的最新编译器,那么就可以使用一些 go1.13 才有的功能。

比如下面的数字分隔符:

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
i := 1_100
fmt.Println(i)
}

然后使用 gotip run main.go 就可以了。

Go: 使用 build tag 来自定义构建配置

通常我们会给每个产品环境设置不同的配置,比如 redis 要在开发环境就连接 localhost:6379,测试环境可能连接某一个主机的 redis。

配置文件通常会使用 env 或者 yml。这样每次构建包放在不同的环境就需要手写一套配置,开发也需要向运维提供配置文档。

最近在一直在用 python,项目通常都是使用 .py 作为配置,直接进行加载,写几份配置,在运行的时候通过命令行参数或者环境变量制定配置加载文件。这样子很大程度减少了开发和运维的沟通成本。

如果放在 go 里面是否可行?因为 go 是编译二进制包,也没有动态加载这么一说,那怎么实现?

这个可以使用 build tags 来自定义配置。

假设我们现在有两个环境,dev 和 prod,那么我们可以新建一个 config 文件夹,放入 dev.go 和 prod.go 两个文件,分别写入对应的配置,如下所示。同一个包,同样的变量名,但是不会因为重复声明和报错,因为这里加了 tag,如下所示:

1
2
3
4
5
6
7
8
// +build dev

package config

// config list
const (
Redis = "redis://127.0.0.1:6379/0"
)
1
2
3
4
5
6
7
// +build prod

package config

const (
Redis = "redis://192.168.0.1:6379"
)

在 main 包引入尝试以下:

1
2
3
4
5
6
$ go build -o=test -tags=dev .
$ ./test
redis://127.0.0.1:6379/0
$ go build -o=test -tags=prod .
$ ./test
redis://192.168.0.1:6379

可以了!

gin-gonic 也有一个编译指令,用于把 encoding/json 包替换为处理速度更快 jsoniter 包,也是使用的构建 tag:$ go build -tags=jsoniter .,实现也很简单,一个加上 // +build jsoniter 另一个默认使用 // +build !jsoniter

这里 tag 前加上 ! 就代表非构建指令下的配置。

tag 常用于交叉编译的配置,例如有些文件针对 linux 而有些文件针对 windows,底层使用的系统调用是不同的,go 源码就包含了很多这样的构建指令:

一行中使用空格就代表“或”的关系,下面指的是在 linux 或者 darwin 环境中使用。

1
// +build linux darwin

如果要指定“与”的关系,那么可以使用,,下面就是指使用 “linux” 和 “cgo” 两个环境同时满足才使用。

1
// +build linux,cgo

当然可以分成多行,下面的指令表述和上面一致:

1
2
// +build linux
// +build cgo

更多还有 ignore 指令来忽略使用这个文件,更多可以查看官方文档,这里不在继续展开。

Python模块和包管理

模块和包

在 Python 中每一个 .py 都可以视作一个模块(module),而每一个包含 __init__.py 的目录则可以视作包(packge)。

如下所示,packs 因为包含 __init__.py 而可以看做是一个包,而 packs/one.py 则是一个模块。

1
2
3
4
5
6
7
├── main.py
├── packs
│ ├── __init__.py
│ └── one.py
└── readme.md

1 directory, 4 files

在 one.py 中简单定义了一个函数 One:

1
2
3
4
5
6
def One():
print("module one")


def OneOne():
print("module one/one")

如果我们想要在 main.py 中使用的话,那么可以直接 import,这种方式也称之为“模块导入”。

1
2
3
import packs

packs.one.One()

这里使用的同时加上了模块名称,当然也可以省略模块名,而这种方式称之为“成员导入”,这种方式比之前一种方式要快一些。

1
2
3
from packs.one import One

One()

当我们运行后就会打印 module one,另外也可以看到在 packs 目录下生成一个 __pycache__ 的新目录,这是编译后中间文件,可以提升模块载入速度。

如果当前模块有多个 One 名称导入的话,可以使用别名进行区分。如果有多个导入名称的话可以使用逗号进行分隔。

1
2
3
4
from packs.one import One as Alias, OneOne

Alias()
OneOne()

如果要在一行导入的名称过多,也可以分行写

1
2
3
4
5
from packs.one import One
from packs.one import OneOne

One()
OneOne()

当然也可以全部导入到当前模块,不过注意这种方式可能存在命名冲突,并且在一些 linter 工具下会提示必须使用全部的导入名称。

1
2
3
4
from packs.one import *

One()
OneOne()

除了这种全局导入外,还可以在局部作用域导入。

1
2
3
4
5
def main():
from packs.one import One
One()

main()

init 文件

这里的 __init__.py 每当导入当前包的模块的时候就会运行一次。

现在在 packs/__init__.py 中加入一行 print("packs one imported") 的语句,然后运行,可以发现 __init__.py 是首先运行的。

1
2
3
$ python3 main.py
packs one imported
module one

根据这个特点,我们可以再 __init__.py 输入导出的模块,外部使用的就不需要很长的导入路径了。

修改 packs/__init__py 为如下所示:

1
2
3
from .one import One

print("packs one imported")

那么在 main.py 中就可以这样使用:

1
2
3
from packs import One

One()

或者这样子:

1
2
3
import packs

packs.One()

在深入一些,我们在 packs 文件夹下新建一个 two 包,然后修改 main.py 并导入这个包,类如下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ tree .
.
├── main.py
├── packs
│ ├── __init__.py
│ ├── one.py
│ └── two
│ ├── __init__.py
│ └── two.py
└── readme.md

2 directories, 6 files

$ cat packs/two/__init__.py
print("packs two imported")

$ cat packs/two/two.py
def Two():
print("module two")

$ cat main.py
from packs.two.two import Two

Two()

接着我们运行 main.py

1
2
3
packs one imported
packs two imported
module two

可以看到两个 __init__ 都被运行了,但是我们还不能使用 packs.one.One 这个函数,因为在 main.py 并没有导入这个名称。

相对路径和绝对路径引用

上面有使用类似这种带有相对路径的导入路径 from .XXX,这种代表从当前的 XXX 模块中导入名称。如果想要在 packs/two/two.py 中使用上一层的 packs/one.py 就可以使用 from ..one import One 的方式。

1
2
3
4
# packs/two/__init__.py
from ..one import One

One()

以此类推,那么 ... 代表更上一级。

但是这种方式还是有问题的,如果项目深度太大就容易写太多 .,还有一种方式就是绝对路径引用,这里的绝对路径是指相对项目根目录而言的,比如上述例子,那么就要修改为:

1
2
3
4
# packs/two/__init__.py
from packs.one import One

One()

那引用当前目录的模块必须使用相对路径了,比如上述例子:

1
2
3
4
5
# packs/two/__init__.py
from packs.one import One
from .two import Two

One()

注意,这里不能是 from two import Two 的形式!这个也好理解,因为绝对路径不是 . 开始的,如果相对路径不使用 . 开始,那么就得从项目根目录开始找了。

当然绝对路径的模块,就有一个 base 路径,所有的文件都是相对此 base 目录,如果在 IDE 中直接打开这里的模块,模块的根目录就是当前模块,显然就会提示找不到了对应的模块了。

模块搜索顺序

自己写的包名肯定可能和第三方或者标准库同名,不过这种同名通常没有问题。因为 python 会优先在当前目录搜索然后在环境变量的搜索路径,之后才是标准库和第三方包。

这个和 linux $PATH 的环境变量一样,按照顺序来搜索。一旦导入每个模块就有全局的命名空间,第二次再次加载就会使用缓存。

这个路径搜索方式和 nodejs 有些区别,nodejs 是一旦同名,优先标准库,如果自定义一个 http 模块,那么永远不会被加载。

pip 包管理工具

这一块比较简单,不过我感觉自己用的也不是特别熟练,这里先写写一般常用的操作。

包管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 安装
# 最新版本
pip install Django
# 指定版本号
pip install Django==2.0.0
# 最小版本
pip install 'Django>=2.0.0'

# 升级包
pip install --upgrade Django
# 卸载包
pip uninstall SomePackage
# 搜索包
pip search SomePackage
# 显示安装包信息
pip show
# 查看指定包的详细信息
pip show -f SomePackage
# 列出已安装的包
pip list
# 查看可升级的包
pip list -o

包依赖锁

1
2
3
pip freeze > requirement.txt # 锁版本
pip install -r requirement.txt # 指定安装版本
pip install --user install black # 安装到用户级目录

使用镜像

1
2
3
pip install -r requirements.txt \
--index-url=https://mirrors.aliyun.com/pypi/simple/ \
--extra-index-url https://pypi.mirrors.ustc.edu.cn/simple/

升级 pip

pip install -U pip 或者sudo easy_install --upgrade pip

减少 Go 构建文件大小的简单策略

由于 go 编译程序自带运行时,所以一个简单的 hello,world 程序也会超过 1M 的大小

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("Hello,world")
}

构建并查看编译后文件大小

1
2
3
$ go build -o 1.out main.go
$ du -h 1.out
2.0M 1.out

最近我发现一个技巧,通过加入 ldflags 指令就可以减少构建大小。

1
2
3
4
$ go build -o 2.out -ldflags '-w -s' main.go
$ du -h 2.out 1.out
1.6M 2.out
2.0M 1.out

减少了 400K 左右。

在 go link 文档中有 ldflags 的指令详细说明,这里就是说把 debug 信息全部移出了。

1
2
3
4
-s
Omit the symbol table and debug information.
-w
Omit the DWARF symbol table.

Go 无文件引入配置到构建程序

程序运行时需要引入一些配置信息,比如需要一个 password 信息。

一般而言有两种方式:1. 使用环境变量 2. 使用配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"encoding/json"
"fmt"
"os"
)

func main() {
// 通过配置文件
var mockFileConf = []byte(`
{
"password" : "conf password"
}
`)

conf := make(map[string]interface{})
if err := json.Unmarshal(mockFileConf, &conf); err != nil {
panic(err)
}
fmt.Println(conf["password"].(string))

// 通过环境变量
fmt.Println(os.Getenv("APP_PASSWORD"))
}

运行结果:

1
2
3
$ APP_PASSWORD="env password" go run main.go
conf password
env password

不过还可以通过硬编码到运行程序内进行,

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

var (
password string
)

func main() {
fmt.Println(password)
}

通过使用 ldflags 参数传入 build 或者 run 里就可以了,

1
2
$ go run -ldflags "-X main.password=password" main.go
password

具体参数说明:

1
2
3
4
5
6
-X importpath.name=value
Set the value of the string variable in importpath named name to value.
This is only effective if the variable is declared in the source code either uninitialized
or initialized to a constant string expression. -X will not work if the initializer makes
a function call or refers to other variables.
Note that before Go 1.5 this option took two separate arguments.

importpath 这里是在 main 包,所以用的是 main.password ,假如是 github.com/islishude/config 这个包,那么对应就是 github.com/islishude/config.password

有几点需要注意:

1 如果是多个配置的话需要使用空格分隔

1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"

var (
username string
password string
)

func main() {
fmt.Println(username, password)
}
1
2
$ go run -ldflags "-X main.username=username -X main.password=password" main.go
username password

2 如果配置内容含有空格,那么需要使用引用。

1
2
$ go run -ldflags "-X 'main.password=build password'" main.go
build password

3 只能使用 string 类型不支持其它类型

1
2
main.num: cannot set with -X: not a var of type string (type.int)
main.bl: cannot set with -X: not a var of type string (type.bool)

这一点很适合,服务端给特定用户生成特定安装包。也适合运维和开发分离的场景。

mongodb update 策略

mongo update() 指令包含是三个参数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
db.collection.update(
query: Document,
update: Document,
config?: {
upsert?: boolean,
multi?: boolean,
writeConcern?: Document,
collation?: Document,
arrayFilters?: Document[]
}
)

假设有如下数据:

1
2
$ db.test.find()
{ "_id" : ObjectId("5ca467fe6e2bdd2db3166bdc"), "name" : "a", "grade" : 1 }

如果使用 update 命令执行如下语句:

1
2
$ db.test.update({ name: 'a' }, { grade: 60 })
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

这里 update 文档仅仅是 kv 数据,而不包含任何“更新运算符”,那么会替换整个文档,但 _id 字段不会被更新。这个时候 update 文档也被称为“替换文档”。

1
2
3
4
5
6
$ db.test.find()
{ "_id" : ObjectId("5ca467fe6e2bdd2db3166bdc"), "name" : 60 }
$ db.test.update({ name: 'a' }, { $set: { grade: 60 }})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
$ db.test.find()
{ "_id" : ObjectId("5ca467fe6e2bdd2db3166bdc"), "name" : "a", "grade" : 60 }

但如果 update 参数包含一个更新运算符的话,比如 $set,那么不会全部替换数据,而是更新已有数据。$set 是更新一个字段值的意思,如果 $set 后的字段不存在,那么就会新建这个字段。

常用的更新运算符除了上面的 $set 还有 $inc 运算符,$inc 后跟正数或负数就代表增加或减少某个值大小。和 $set 一样,如果这个字段不存在,那么就会新建这个字段。不过如果这个字段不是数值类型那么就会报错。

1
2
3
4
5
6
7
8
9
10
$ db.test.update({ name: 'a' }, { $inc: { name: 100}})
WriteResult({
"nMatched" : 0,
"nUpserted" : 0,
"nModified" : 0,
"writeError" : {
"code" : 14,
"errmsg" : "Cannot apply $inc to a value of non-numeric type. {_id: ObjectId('5ca467fe6e2bdd2db3166bdc')} has the field 'name' of non-numeric type string"
}
})

update 函数可以使用第三个参数作为配置,常用的是 upsert,如果设置 upserttrue,那么如果这条记录不存在就会 insert 一条数据。

1
2
3
4
$ db.test.find()
$ db.test.update({ name: 'b' }, { name: 'b', grade: 50 }, { $upsert: true } )
$ db.test.find()
{ "_id" : ObjectId("5ca467fe6e2bdd2db3166bdc"), "name" : "b", "grade" : 50 }

如果 update() 第二个参数 update 文档是替换文档,也就是说不包含任何更新运算符,那么就会直接使用替换文档插入一条数据,当然没有 _id 字段就会生这个字段。

1
2
3
4
5
6
7
8
9
$ db.test.update({ name: 'c'}, { grade: 100 }, { upsert: true })
WriteResult({
"nMatched" : 0,
"nUpserted" : 1,
"nModified" : 0,
"_id" : ObjectId("5ca472432832f7853835cf0d")
})
$ db.test.find()
{ "_id" : ObjectId("5ca472432832f7853835cf0d"), "grade" : 100 }

如果 update 参数包含了更新运算符的非替换文档,那么会使用 query 文档作为基础文档,然后使用 update 文档进行更新操作。

1
2
3
4
5
6
7
8
9
$ db.test.update({ name: 'd' }, { $set: { grade: 1 }}, { upsert: true })
WriteResult({
"nMatched" : 0,
"nUpserted" : 1,
"nModified" : 0,
"_id" : ObjectId("5ca473352832f7853835cf1e")
})
$ db.test.find()
{ "_id" : ObjectId("5ca473352832f7853835cf1e"), "name" : "d", "grade" : 1 }

但是如果 query 文档内包含“比较运算符”的话,那么就不会被把 query 文档包含进新文档内。

1
2
3
4
5
6
7
8
9
> db.test.update({ grade: { $lt: 100 }}, { $set: { name: 'a' }}, { upsert: true })
WriteResult({
"nMatched" : 0,
"nUpserted" : 1,
"nModified" : 0,
"_id" : ObjectId("5ca4744f2832f7853835cf38")
})
> db.test.find()
{ "_id" : ObjectId("5ca4744f2832f7853835cf38"), "name" : "a" }

不过由于 { key: { $eq: value} } 比较运算符等同于 { key: value },这里就会把 query 文档包含进新文档。

1
2
3
4
5
6
7
8
9
$ db.test.update({ name: { $eq: 'e' }}, { $set:{ grade: 100 }}, { upsert: true})
WriteResult({
"nMatched" : 0,
"nUpserted" : 1,
"nModified" : 0,
"_id" : ObjectId("5ca473dd2832f7853835cf2d")
})
$ db.test.find()
{ "_id" : ObjectId("5ca473dd2832f7853835cf2d"), "name" : "e", "grade" : 100 }

使用 Go 进行 Solidity ABI 编解码

类型对应关系

类型 Solidity Go
字符串 string string
布尔 bool bool
地址 address common.Address
无符号整数 uintN uintN 或 *big.Int
有符号整数 intN intN 或 *big.Int
固定长度字节数组 bytesN [N]byte
动态长度字节数组 bytes []byte
固定长度数组 T[k] array
动态长度数组 T[] slice
枚举 enum uintN
映射 mapping -
结构体 struct -

备注:

  • solidity 中 uintN 和 intN 类型如果和 go 内置类型名相同,那么就一一对应,否则就是 *big.Int 类型。比如说 Solidity uint8 对应 go 的 uint8 而 solidity 中 uint256 以及 uint160 等就对应 go *big.Int 类型
  • 固定长度数组对应相应类型数组,比如 Solidity int[2] 对应 go 的 [2]int
  • 动态长度数组对应相应类型的切片,比如 Solidity 的 int[] 对应 go 的 []int
  • 枚举对应一个无符号的整数,具体根据枚举数量,一般为 uint8 类型
  • mapping 只能使用 storage 存储类型,不能作为函数参数和函数作用域变量,只能用于状态变量
  • struct 结构体 ABI 编解码目前处于实验阶段

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package main

import (
"encoding/json"
"fmt"
"math/big"
"os"
"strings"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
)

/*
pragma solidity ^0.6.0;

interface ABI {
function List(address owner) external view returns (address[] memory receiver, uint256[] memory values);
function Value(address owner) external view returns (uint256 values);
}
*/

const RawABI = `[
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "List",
"outputs": [
{
"internalType": "address[]",
"name": "receiver",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "values",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "Value",
"outputs": [
{
"internalType": "uint256",
"name": "values",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]`

func main() {
parsed, err := abi.JSON(strings.NewReader(RawABI))
if err != nil {
panic(err)
}

{
address := common.HexToAddress("0x80819B3F30e9D77DE6BE3Df9d6EfaA88261DfF9c")

// Value 参数编码
valueInput, err := parsed.Pack("Value", address)
if err != nil {
panic(err)
}

// Value 参数解码
var addrwant common.Address
if err := parsed.Methods["Value"].Inputs.Unpack(&addrwant, valueInput[4:]); err != nil {
panic(err)
}
fmt.Println("should equals", addrwant == address)

// Value 返回值解码
var balance *big.Int
var returns = common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000005f5e100")
if err := parsed.Unpack(&balance, "Value", returns); err != nil {
panic(err)
}
fmt.Println("Value 返回值", balance)
}

// List 返回值编码
{
// 注意:字段名称需要与 ABI 编码的定义的一致
// 比如,这里 ABI 编码返回值第一个为 receiver 那么转化为 Go 就是首字母大写的 Receiver
var res struct {
Receiver []common.Address // 返回值名称
Values []*big.Int // 返回值名称
}

// {"Receiver":["0x80819b3f30e9d77de6be3df9d6efaa88261dff9c"],"Values":[10]}
raw := common.Hex2Bytes("00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000080819b3f30e9d77de6be3df9d6efaa88261dff9c0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a")
if err := parsed.Unpack(&res, "List", raw); err != nil {
panic(err)
}
_ = json.NewEncoder(os.Stdout).Encode(&res)
}
}

Dockerfile COPY 和 ADD 指令区别和使用规则

ADD 和 COPY 指令在 Dockerfile 中具有相同的功能,都是将构建上下文的文件复制到镜像中去,具体语法规则如下所示,可以说在大多数情况下,二者仅仅是语义上的区别。

1
2
3
4
5
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]

COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]

其中 src(下称源路径) 和 dest(下称目标路径) 参数可以是目录或者文件,下面使用具体事例来说明具体的规则。

新建一个文件夹,具体包含内容如下所示

1
2
3
4
5
6
7
8
9
$ tree .
.
├── Dockerfile
├── README.md
├── cmd
│ └── main.go
└── main.go

1 directory, 4 files

源路径和目标路径都是目录

在 Dockerfile 的规则中,如果目标路径最后跟 “/“ 符号,那么就代表目录,否则就是文件。如果目标目录不存在,那么会新建这个目录。下面例子使用 alpine 镜像作为示例,运行此镜像并查看其根目录,如下所示并没有一个 “app” 的目录,那么接下的例子中目标路径名称都用 “app” 来表示。

1
2
3
$ docker run -it --rm alpine ls /
bin etc lib mnt proc run srv tmp var
dev home media opt root sbin sys usr

修改 Dockerfile,如下所示,使其复制 cmd 文件夹到 app 中。

1
2
3
FROM alpine:latest
COPY cmd app/
CMD ["sh"]

那么这里我们进行构建镜像,然后查看镜像中发生了什么,如下所示,使用 test 作为镜像名称并构建成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ docker build -t test .
Sending build context to Docker daemon 3.584kB
Step 1/3 : FROM alpine:latest
latest: Pulling from library/alpine
8e402f1a9c57: Already exists
Digest: sha256:c43263c39b952a419a4f6e2152b6c0afc7f765d9e6660e512a34ee14caccce02
Status: Downloaded newer image for alpine:latest
---> 5cb3aa00f899
Step 2/3 : COPY cmd app/
---> 8ca0d3bbf0a5
Step 3/3 : CMD ["sh"]
---> Running in 600926c6e043
Removing intermediate container 600926c6e043
---> a03ff8077b4c
Successfully built a03ff8077b4c
Successfully tagged test:latest

现在运行并进入容器,如下所示,根目录下多了 “app” 文件夹,并且 “app” 文件夹内只有源文件夹下的 “main.go” 文件,并不包含其所在的文件夹 “cmd”。

1
2
3
4
5
6
7
8
$ docker run --rm -it test sh
$ ls
app bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
$ ls -al app
total 12
drwxr-xr-x 2 root root 4096 Mar 27 01:57 .
drwxr-xr-x 1 root root 4096 Mar 27 01:57 ..
-rw-r--r-- 1 root root 181 Mar 27 01:57 main.go

这里是非常重要的规则,源路径如果是目录,那么只复制其内部的文件而不包含自身,另外文件自身的文件系统元数据也将复制过去,比如说文件权限等。

源路径是文件夹而目录路径是文件

修改 Dockerfile 为下面内容,只是把目标路径后的 “/“ 去掉了。

1
2
3
4
FROM alpine:latest
- COPY cmd app/
+ COPY cmd app
CMD ["sh"]

结果竟然成功了,和目标路径是文件夹并没有区别。

1
2
3
4
5
6
7
8
$ docker run --rm -it test
$ ls
app bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
$ ls -al app
total 16
drwxr-xr-x 2 root root 4096 Mar 27 02:33 .
drwxr-xr-x 1 root root 4096 Mar 27 02:33 ..
-rw-r--r-- 1 root root 181 Mar 27 01:57 main.go

也就是说如果目标路径不包含 “/“ ,那么将会把目标路径视作普通文件,然后会将源路径的所有文件写入目标路径。这里目标路径虽然是文件但也被作为目录构建了。虽然上述可以使用,但是不推荐使用,因为这个语义并不明确,而且在多个源文件的情况下会报错。

如下所示,将本文件夹下的 “main.go” 和 “README.md” 复制到镜像的 “app” 下。

1
2
3
FROM alpine:latest
COPY main.go README.md app
CMD ["sh"]

这里就报错了,提示目录路径不是目录。

1
2
3
4
5
6
$ docker build -t test .
Sending build context to Docker daemon 10.75kB
Step 1/3 : FROM alpine:latest
---> 5cb3aa00f899
Step 2/3 : COPY main.go README.md app
When using COPY with more than one source file, the destination must be a directory and end with a /

源路径是文件而目标路径是目录

修改 Dockerfile 为下面的内容,将上面出错的例子改一下,将目标路径后加上 “/“,使其变成语义上的目录

1
2
3
4
FROM alpine:latest
- COPY main.go README.md app
+ COPY main.go README.md app/
CMD ["sh"]

这回就成功了,并没有错误了,如下所示:

1
2
3
4
5
6
7
8
$ docker run --rm -it test
$ ls
app bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
$ ls -al app
total 16
drwxr-xr-x 2 root root 4096 Mar 27 02:50 .
drwxr-xr-x 1 root root 4096 Mar 27 02:57 ..
-rw-r--r-- 1 root root 181 Mar 27 01:57 main.go

当然也可以使用通配符进行匹配源文件,下面的例子是复制所有的 go 文件到目标路径下。

1
2
3
FROM alpine:latest
COPY *.go app/
CMD ["sh"]

源路径和目标路径都是文件

修改 Dockerfile 为下面的内容,使得复制 “main.go” 文件到 “app”,不过再此之前我们修改下权限属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cat main.go
package main

import (
"fmt"
)

func main() {
fmt.Println("Hello,World")
}
$ ls -al main.go
-rw-r--r-- 1 lishude staff 77 Mar 27 11:16 main.go
$ chmod 666 main.go
$ ls -al main.go
-rw-rw-rw- 1 lishude staff 77 Mar 27 11:16 main.go

然后修改 Dockerfile 为下面的内容

1
2
3
4
FROM alpine:latest
- COPY main.go README.md app/
+ COPY main.go app
CMD ["sh"]

查看内容,如下所示,重命名为了 “app”,按照文档的所说,只是将源路径的文件内容写入了目标路径文件,仍旧保留了其文件元数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker run -it --rm test
$ ls
app bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
$ ls -al app
-rw-rw-rw- 1 root root 77 Mar 27 03:16 app
$ cat app
package main

import (
"fmt"
)

func main() {
fmt.Println("Hello,World")
}

ADD 指令源路径是网络地址

ADD 指令除了 COPY 指令的简单复制功能外,还支持从网络地址上下载。如下所示,修改 Dockerfile 文件,这里选择文件大小比较小的 vuejs 最新源码打包文件作为源路径。

1
2
3
FROM alpine:latest
ADD https://github.com/vuejs/vue/archive/v2.6.10.tar.gz app/
CMD ["sh"]

然后进行镜像构建,可以看到 ADD 命令进行了文件下载,注意:这个是 COPY 命令不支持的。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ docker build -t test .
Sending build context to Docker daemon 12.8kB
Step 1/3 : FROM alpine:latest
---> 5cb3aa00f899
Step 2/3 : ADD https://github.com/vuejs/vue/archive/v2.6.10.tar.gz app/
Downloading [==================================================>] 1.576MB/1.576MB
---> 5c8b2c2ba33d
Step 3/3 : CMD ["sh"]
---> Running in 984e336dc0d8
Removing intermediate container 984e336dc0d8
---> 58a7a28bddeb
Successfully built 58a7a28bddeb
Successfully tagged test:latest

查看镜像构建的结果,如下所示,默认情况下,下载的文件的权限为 600,如果这不是想要的权限,那么可以再加一层 RUN 指令进行修改。

1
2
3
4
5
6
$ docker run -ti --rm test
$ ls -al app
total 1548
drwxr-xr-x 2 root root 4096 Mar 27 03:28 .
drwxr-xr-x 1 root root 4096 Mar 27 03:28 ..
-rw------- 1 root root 1576461 Jan 1 1970 v2.6.10.tar.gz

接着如果目标路径是文件呢?修改 Dockerfile 并查看其内容,结果如下,仅仅修改了文件名称而已。

1
2
$ ls -al app
-rw------- 1 root root 1576461 Jan 1 1970 app

ADD 源路径是打包压缩文件

使用 wget 命令下载 vue.tar.gz 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ wget https://github.com/vuejs/vue/archive/v2.6.10.tar.gz
--2019-03-27 11:40:21-- https://github.com/vuejs/vue/archive/v2.6.10.tar.gz
Resolving github.com (github.com)... 13.250.177.223, 13.229.188.59, 52.74.223.119
Connecting to github.com (github.com)|13.250.177.223|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://codeload.github.com/vuejs/vue/tar.gz/v2.6.10 [following]
--2019-03-27 11:40:22-- https://codeload.github.com/vuejs/vue/tar.gz/v2.6.10
Resolving codeload.github.com (codeload.github.com)... 54.251.140.56, 13.250.162.133, 13.229.189.0
Connecting to codeload.github.com (codeload.github.com)|54.251.140.56|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1576461 (1.5M) [application/x-gzip]
Saving to: ‘v2.6.10.tar.gz’

v2.6.10.tar.gz 100%[===========================>] 1.50M 313KB/s in 5.7s

2019-03-27 11:40:29 (271 KB/s) - ‘v2.6.10.tar.gz’ saved [1576461/1576461]
$ ls -al v2.6.10.tar.gz
-rw-r--r-- 1 lishude staff 1576461 Mar 27 11:40 v2.6.10.tar.gz

然后修改文件 Dockerfile 为:

1
2
3
FROM alpine:latest
ADD v2.6.10.tar.gz app/
CMD ["sh"]

构建并查看文件内容,这里 Docker 已经解压了这个文件,而上一个例子中也是下载的打包压缩文件是不支持自动解压的。

1
2
3
4
5
6
7
8
9
$ ls
app bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
$ cd app
/app # ls
vue-2.6.10
$ cd vue-2.6.10/
$ ls
BACKERS.md README.md dist flow packages src types
LICENSE benchmarks examples package.json scripts test yarn.lock

根据文档所说明,如果源路径为一个压缩格式为 gzip, bzip2 以及 xz 的情况下的文件,ADD 指令将会将这个压缩文件到解压成到目标路径。

总结

看起来 ADD 指令要比 COPY 指令功能更加多,但是根据 Docker 最佳实践的说明,除非需要解压缩功能,否则要尽可能的使用 COPY 指令,因为 COPY 的语义很明确,就是复制文件而已。

禁用 Mac 内置键盘和触控板

最近买了个键盘,大小刚好能放在 13 英寸的 MBP 上,这样打字就舒服了很多。但是存在一个问题,直接放在上面有时候会触发内置键盘的按键。

解决方式就是禁用内置键盘,找了好多解决方案,结果都不能使用,最后找到这个软件 tekezo/Karabiner-Elements。安装后会安装两个软件,一个 Karabiner-Elements 和 Karabiner-EventViewer,要禁用内置键盘只需要用到第一个软件即可。

使用十分简单,打开软件切换到 Device 标签页,在下方的 Disable the built-in keyboard... 选择外接键盘即可,这个设置会在外接键盘接入时候禁用内置键盘。

屏幕快照 2019-03-25 上午11 10 24

顺便一提,这个软件是开源的,使用中可以安心放行一些系统权限。

keyword: disable the built-in Macbook keyboard