docker命令之login

docker version:1.0.0

位置:login命令的源碼位於/docker/api/client/command.go中

使用方式:docker login [-e|-email=""] [-p|--password=""] [-u|--username=""] [SERVER]

功能:用於登錄registry

eg. docker login 127.0.0.1:5000

=================

以下是在client端執行login的過程,通過api的CmdLogin接口,組建POST /auth,將此請求發送給docker deamon的http server

http server將此請求路由到postAuth(api/server/server.go),創建一個名爲auth的job。




================================

------------------------dcoker client組建POST auth請求----------------------------

func (cli *DockerCli) CmdLogin(args ...string) error {
cmd := cli.Subcmd("login", "[SERVER]", "Register or log in to a Docker registry server, if no server is specified \""+registry.IndexServerAddress()+"\" is the default.")

var username, password, email string

cmd.StringVar(&username, []string{"u", "-username"}, "", "Username")
cmd.StringVar(&password, []string{"p", "-password"}, "", "Password")
cmd.StringVar(&email, []string{"e", "-email"}, "", "Email")
 //分析輸入的參數

err := cmd.Parse(args)
if err != nil {
return nil
}

//如果命令行中沒有registry地址,那麼獲取默認的registry index server的地址:const INDEXSERVER = "https://index.docker.io/v1/"

//如果有將使用命令行中的registry地址
serverAddress := registry.IndexServerAddress()
if len(cmd.Args()) > 0 {
serverAddress = cmd.Arg(0) 
}

//提示函數
promptDefault := func(prompt string, configDefault string) {
if configDefault == "" {
fmt.Fprintf(cli.out, "%s: ", prompt)
} else {
fmt.Fprintf(cli.out, "%s (%s): ", prompt, configDefault)
}
}

// 讀取輸入
readInput := func(in io.Reader, out io.Writer) string {
reader := bufio.NewReader(in)
line, _, err := reader.ReadLine()
if err != nil {
fmt.Fprintln(out, err.Error())
os.Exit(1)
}
return string(line)
}

//獲取配置文件
cli.LoadConfigFile()   //獲取“HOME”目錄下的registry授權信息文件.dockercfg      const CONFIGFILE = ".dockercfg"

//在授權配置文件中沒有找到registry項,新建此registry的配置項
authconfig, ok := cli.configFile.Configs[serverAddress]
if !ok {
authconfig = registry.AuthConfig{}
}

//填充username
if username == "" {
promptDefault("Username", authconfig.Username)
username = readInput(cli.in, cli.out)
if username == "" {
username = authconfig.Username
}
}

//填充passwd和email
if username != authconfig.Username {
if password == "" {
oldState, _ := term.SaveState(cli.terminalFd)
fmt.Fprintf(cli.out, "Password: ")
term.DisableEcho(cli.terminalFd, oldState)

password = readInput(cli.in, cli.out)
fmt.Fprint(cli.out, "\n")

term.RestoreTerminal(cli.terminalFd, oldState)
if password == "" {
return fmt.Errorf("Error : Password Required")
}
}

if email == "" {
promptDefault("Email", authconfig.Email)
email = readInput(cli.in, cli.out)
if email == "" {
email = authconfig.Email
}
}
} else {
password = authconfig.Password
email = authconfig.Email
}

//將新的授權信息添加授權結構體中
authconfig.Username = username
authconfig.Password = password
authconfig.Email = email
authconfig.ServerAddress = serverAddress
cli.configFile.Configs[serverAddress] = authconfig

//向registry發起POST /auth請求,在registry的index中進行授權方面的認證過程
stream, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[serverAddress], false)
if statusCode == 401 {
delete(cli.configFile.Configs, serverAddress)
registry.SaveConfig(cli.configFile)
return err
}
if err != nil {
return err
}
var out2 engine.Env
err = out2.Decode(stream)
if err != nil {
cli.configFile, _ = registry.LoadConfig(os.Getenv("HOME"))
return err
}

//請求成功,保存授權信息到.dockercfg中
registry.SaveConfig(cli.configFile)
if out2.Get("Status") != "" {
fmt.Fprintf(cli.out, "%s\n", out2.Get("Status"))
}
return nil
}

----------------------------------向docker deamon的http server發起POST /auth請求---------------------------------------------------------------

位置:cil.call位於/api/client/utils.go


stream, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[serverAddress], false)

func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo bool) (io.ReadCloser, int, error) {

//解析registry地址
params, err := cli.encodeData(data)
if err != nil {
return nil, -1, err
}

//新建方法爲POST路徑爲/auth的http請求
req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", api.APIVERSION, path), params)
if err != nil {
return nil, -1, err
}

//若registry地址存在,獲取index地址信息
if passAuthInfo {
cli.LoadConfigFile()
// Resolve the Auth config relevant for this server
authConfig := cli.configFile.ResolveAuthConfig(registry.IndexServerAddress()) //獲取index地址信息 const INDEXSERVER = "https://index.docker.io/v1/"

//組裝http請求的頭部
getHeaders := func(authConfig registry.AuthConfig) (map[string][]string, error) {
buf, err := json.Marshal(authConfig)
if err != nil {
return nil, err
}
registryAuthHeader := []string{
base64.URLEncoding.EncodeToString(buf),
}
return map[string][]string{"X-Registry-Auth": registryAuthHeader}, nil
}


if headers, err := getHeaders(authConfig); err == nil && headers != nil {
for k, v := range headers {
req.Header[k] = v
}
}
}
req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION)
req.URL.Host = cli.addr
req.URL.Scheme = cli.scheme
if data != nil {
req.Header.Set("Content-Type", "application/json")
} else if method == "POST" {
req.Header.Set("Content-Type", "plain/text")
}

//發起POST /auth請求, 在真正發起請求錢,做了個ping的動作
resp, err := cli.HTTPClient().Do(req)
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return nil, -1, ErrConnectionRefused
}
return nil, -1, err
}
//請求返回錯誤碼處理
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, -1, err
}
if len(body) == 0 {
return nil, resp.StatusCode, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(resp.StatusCode), req.URL)
}
return nil, resp.StatusCode, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body))
}
//返回index服務響應的包體和狀態碼
return resp.Body, resp.StatusCode, nil
}

------------------------------------docker deamon的http server處理client端的POST auth請求---------------------------

代碼位置 api/server/server.go

func postAuth(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {

//獲取認證配置,新建名爲auth的job
var (
authConfig, err = ioutil.ReadAll(r.Body)
job = eng.Job("auth")
stdoutBuffer = bytes.NewBuffer(nil)
)
if err != nil {
return err
}
job.Setenv("authConfig", string(authConfig))
job.Stdout.Add(stdoutBuffer)

//運行auth job
if err = job.Run(); err != nil {
return err
}
if status := engine.Tail(stdoutBuffer, 1); status != "" {
var env engine.Env
env.Set("Status", status)
return writeJSON(w, http.StatusOK, env)
}
w.WriteHeader(http.StatusNoContent)
return nil
}

--------------------------------------auth job調用registry包中的auth.go中的Login方法向registry發起用戶認證-------------------------

registry包中向引擎eng註冊的handler,使用s.Auth處理。

//代碼位置registry/service.go

// Install installs registry capabilities to eng.
func (s *Service) Install(eng *engine.Engine) error {
eng.Register("auth", s.Auth)
eng.Register("search", s.Search)
return nil
}

func (s *Service) Auth(job *engine.Job) engine.Status {
var (
err error
authConfig = &AuthConfig{}
)

job.GetenvJson("authConfig", authConfig)
// TODO: this is only done here because auth and registry need to be merged into one pkg
if addr := authConfig.ServerAddress; addr != "" && addr != IndexServerAddress() {
addr, err = ExpandAndVerifyRegistryUrl(addr)
if err != nil {
return job.Error(err)
}
authConfig.ServerAddress = addr
}
//使用registry/auth.go中的Login處理

status, err := Login(authConfig, HTTPRequestFactory(nil))
if err != nil {
return job.Error(err)
}
job.Printf("%s\n", status)
return engine.StatusOK
}

----------------------registry/auth.go---------------------

docker客戶端向docker-registry發起認證請求使用POST方法 /v1/users路由,

docker-registry收到請求後,進行註冊,若註冊成功返回給docker客戶端201,並提示激活用戶

若返回400,則用戶已經註冊


// try to register/login to the registry server
func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, error) {
var (
status string
reqBody []byte
err error
client = &http.Client{
Transport: &http.Transport{
DisableKeepAlives: true,
Proxy: http.ProxyFromEnvironment,
},
CheckRedirect: AddRequiredHeadersToRedirectedRequests,
}
reqStatusCode = 0
serverAddress = authConfig.ServerAddress //使用命令中提供的registry地址
)

if serverAddress == "" {
serverAddress = IndexServerAddress() //如果命令行中沒有提供registry地址,使用官方的registry地址
}

loginAgainstOfficialIndex := serverAddress == IndexServerAddress()

// to avoid sending the server address to the server it should be removed before being marshalled
authCopy := *authConfig
authCopy.ServerAddress = ""

jsonBody, err := json.Marshal(authCopy)
if err != nil {
return "", fmt.Errorf("Config Error: %s", err)
}

// using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status.
b := strings.NewReader(string(jsonBody))
req1, err := http.Post(serverAddress+"users/", "application/json; charset=utf-8", b)//向registry發起POST方法,路由爲/v1/users的請求
if err != nil {
return "", fmt.Errorf("Server Error: %s", err)
}
reqStatusCode = req1.StatusCode
defer req1.Body.Close()
reqBody, err = ioutil.ReadAll(req1.Body)
if err != nil {
return "", fmt.Errorf("Server Error: [%#v] %s", reqStatusCode, err)
}
//下面爲分析registry返回的狀態碼的情況
if reqStatusCode == 201 {//收到201狀態碼,代碼用戶註冊成功,提示用戶激活,退出,不做登錄操作
if loginAgainstOfficialIndex { //如果是docker官方的registry,提示信息
status = "Account created. Please use the confirmation link we sent" +
" to your e-mail to activate it."
} else { //如果是私有的registry,提示信息
status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it."
}
} else if reqStatusCode == 400 { //收到400,代表用戶已存在,發起GET方法,路由爲/v1/users的請求,進行用戶登錄(這個也許是docker官方的registry的狀態碼,私有的返回401)
if string(reqBody) == "\"Username or email already exists\"" {
req, err := factory.NewRequest("GET", serverAddress+"users/", nil)
req.SetBasicAuth(authConfig.Username, authConfig.Password)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}

//發起登錄請求後,若收到200,代表用戶登錄成功
if resp.StatusCode == 200 {
status = "Login Succeeded"
} else if resp.StatusCode == 401 { //發起登錄請求後,收到401,代表用戶名或密碼錯誤
return "", fmt.Errorf("Wrong login/password, please try again")
} else if resp.StatusCode == 403 { //發起登錄請求後,收到403,代表賬戶沒有激活
if loginAgainstOfficialIndex { //如果是docker官方的registry,提示信息
return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.")
}
return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)
} else { //收到其他狀態碼的提示信息
return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header)
}
} else { //註冊時收到其他狀態信息時
return "", fmt.Errorf("Registration: %s", reqBody)
}
} else if reqStatusCode == 401 {//註冊時收到401,代表登錄私有registry的用戶登錄
// This case would happen with private registries where /v1/users is
// protected, so people can use `docker login` as an auth check.
req, err := factory.NewRequest("GET", serverAddress+"users/", nil)


req.SetBasicAuth(authConfig.Username, authConfig.Password)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode == 200 { //登錄私有registry,若收到200,代表登錄成功
status = "Login Succeeded"
} else if resp.StatusCode == 401 {//登錄私有registry,若收到401,代表用戶名或密碼錯誤
return "", fmt.Errorf("Wrong login/password, please try again")
} else {
return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
resp.StatusCode, resp.Header)
}
} else {
return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody)
}
return status, nil
}

registry使用standalone方式,從docker client訪問registry協議流程圖


1.client向registry發起ping操作,使用GET方法,協議版本爲V1,試探是否能夠和registry通信

包體內容爲

2.registry向client返回200 OK,代碼已經收到client的ping操作請求

包體爲

3.client將登錄的用戶信息交給registry端,適應POST方法代碼要求registry創建此用戶,若此用戶存在,則返回401

4.registry返回401錯誤碼,代表此用戶已經存在

5.client收到401,知道了registry端已有此用戶,client使用GET方法,攜帶basic authorization驗證用戶信息

6、registry給client返回200 OK,代表此用戶通過驗證

發佈了17 篇原創文章 · 獲贊 4 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章