mirror of https://github.com/k3d-io/k3d
parent
bb1f5bde71
commit
f59216c2e0
@ -0,0 +1,21 @@ |
|||||||
|
The MIT License (MIT) |
||||||
|
|
||||||
|
Copyright (c) 2015 Microsoft Corporation |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in |
||||||
|
all copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||||
|
THE SOFTWARE. |
@ -0,0 +1,12 @@ |
|||||||
|
# go-ansiterm |
||||||
|
|
||||||
|
This is a cross platform Ansi Terminal Emulation library. It reads a stream of Ansi characters and produces the appropriate function calls. The results of the function calls are platform dependent. |
||||||
|
|
||||||
|
For example the parser might receive "ESC, [, A" as a stream of three characters. This is the code for Cursor Up (http://www.vt100.net/docs/vt510-rm/CUU). The parser then calls the cursor up function (CUU()) on an event handler. The event handler determines what platform specific work must be done to cause the cursor to move up one position. |
||||||
|
|
||||||
|
The parser (parser.go) is a partial implementation of this state machine (http://vt100.net/emu/vt500_parser.png). There are also two event handler implementations, one for tests (test_event_handler.go) to validate that the expected events are being produced and called, the other is a Windows implementation (winterm/win_event_handler.go). |
||||||
|
|
||||||
|
See parser_test.go for examples exercising the state machine and generating appropriate function calls. |
||||||
|
|
||||||
|
----- |
||||||
|
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. |
@ -0,0 +1,188 @@ |
|||||||
|
package ansiterm |
||||||
|
|
||||||
|
const LogEnv = "DEBUG_TERMINAL" |
||||||
|
|
||||||
|
// ANSI constants
|
||||||
|
// References:
|
||||||
|
// -- http://www.ecma-international.org/publications/standards/Ecma-048.htm
|
||||||
|
// -- http://man7.org/linux/man-pages/man4/console_codes.4.html
|
||||||
|
// -- http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
|
||||||
|
// -- http://en.wikipedia.org/wiki/ANSI_escape_code
|
||||||
|
// -- http://vt100.net/emu/dec_ansi_parser
|
||||||
|
// -- http://vt100.net/emu/vt500_parser.svg
|
||||||
|
// -- http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||||
|
// -- http://www.inwap.com/pdp10/ansicode.txt
|
||||||
|
const ( |
||||||
|
// ECMA-48 Set Graphics Rendition
|
||||||
|
// Note:
|
||||||
|
// -- Constants leading with an underscore (e.g., _ANSI_xxx) are unsupported or reserved
|
||||||
|
// -- Fonts could possibly be supported via SetCurrentConsoleFontEx
|
||||||
|
// -- Windows does not expose the per-window cursor (i.e., caret) blink times
|
||||||
|
ANSI_SGR_RESET = 0 |
||||||
|
ANSI_SGR_BOLD = 1 |
||||||
|
ANSI_SGR_DIM = 2 |
||||||
|
_ANSI_SGR_ITALIC = 3 |
||||||
|
ANSI_SGR_UNDERLINE = 4 |
||||||
|
_ANSI_SGR_BLINKSLOW = 5 |
||||||
|
_ANSI_SGR_BLINKFAST = 6 |
||||||
|
ANSI_SGR_REVERSE = 7 |
||||||
|
_ANSI_SGR_INVISIBLE = 8 |
||||||
|
_ANSI_SGR_LINETHROUGH = 9 |
||||||
|
_ANSI_SGR_FONT_00 = 10 |
||||||
|
_ANSI_SGR_FONT_01 = 11 |
||||||
|
_ANSI_SGR_FONT_02 = 12 |
||||||
|
_ANSI_SGR_FONT_03 = 13 |
||||||
|
_ANSI_SGR_FONT_04 = 14 |
||||||
|
_ANSI_SGR_FONT_05 = 15 |
||||||
|
_ANSI_SGR_FONT_06 = 16 |
||||||
|
_ANSI_SGR_FONT_07 = 17 |
||||||
|
_ANSI_SGR_FONT_08 = 18 |
||||||
|
_ANSI_SGR_FONT_09 = 19 |
||||||
|
_ANSI_SGR_FONT_10 = 20 |
||||||
|
_ANSI_SGR_DOUBLEUNDERLINE = 21 |
||||||
|
ANSI_SGR_BOLD_DIM_OFF = 22 |
||||||
|
_ANSI_SGR_ITALIC_OFF = 23 |
||||||
|
ANSI_SGR_UNDERLINE_OFF = 24 |
||||||
|
_ANSI_SGR_BLINK_OFF = 25 |
||||||
|
_ANSI_SGR_RESERVED_00 = 26 |
||||||
|
ANSI_SGR_REVERSE_OFF = 27 |
||||||
|
_ANSI_SGR_INVISIBLE_OFF = 28 |
||||||
|
_ANSI_SGR_LINETHROUGH_OFF = 29 |
||||||
|
ANSI_SGR_FOREGROUND_BLACK = 30 |
||||||
|
ANSI_SGR_FOREGROUND_RED = 31 |
||||||
|
ANSI_SGR_FOREGROUND_GREEN = 32 |
||||||
|
ANSI_SGR_FOREGROUND_YELLOW = 33 |
||||||
|
ANSI_SGR_FOREGROUND_BLUE = 34 |
||||||
|
ANSI_SGR_FOREGROUND_MAGENTA = 35 |
||||||
|
ANSI_SGR_FOREGROUND_CYAN = 36 |
||||||
|
ANSI_SGR_FOREGROUND_WHITE = 37 |
||||||
|
_ANSI_SGR_RESERVED_01 = 38 |
||||||
|
ANSI_SGR_FOREGROUND_DEFAULT = 39 |
||||||
|
ANSI_SGR_BACKGROUND_BLACK = 40 |
||||||
|
ANSI_SGR_BACKGROUND_RED = 41 |
||||||
|
ANSI_SGR_BACKGROUND_GREEN = 42 |
||||||
|
ANSI_SGR_BACKGROUND_YELLOW = 43 |
||||||
|
ANSI_SGR_BACKGROUND_BLUE = 44 |
||||||
|
ANSI_SGR_BACKGROUND_MAGENTA = 45 |
||||||
|
ANSI_SGR_BACKGROUND_CYAN = 46 |
||||||
|
ANSI_SGR_BACKGROUND_WHITE = 47 |
||||||
|
_ANSI_SGR_RESERVED_02 = 48 |
||||||
|
ANSI_SGR_BACKGROUND_DEFAULT = 49 |
||||||
|
// 50 - 65: Unsupported
|
||||||
|
|
||||||
|
ANSI_MAX_CMD_LENGTH = 4096 |
||||||
|
|
||||||
|
MAX_INPUT_EVENTS = 128 |
||||||
|
DEFAULT_WIDTH = 80 |
||||||
|
DEFAULT_HEIGHT = 24 |
||||||
|
|
||||||
|
ANSI_BEL = 0x07 |
||||||
|
ANSI_BACKSPACE = 0x08 |
||||||
|
ANSI_TAB = 0x09 |
||||||
|
ANSI_LINE_FEED = 0x0A |
||||||
|
ANSI_VERTICAL_TAB = 0x0B |
||||||
|
ANSI_FORM_FEED = 0x0C |
||||||
|
ANSI_CARRIAGE_RETURN = 0x0D |
||||||
|
ANSI_ESCAPE_PRIMARY = 0x1B |
||||||
|
ANSI_ESCAPE_SECONDARY = 0x5B |
||||||
|
ANSI_OSC_STRING_ENTRY = 0x5D |
||||||
|
ANSI_COMMAND_FIRST = 0x40 |
||||||
|
ANSI_COMMAND_LAST = 0x7E |
||||||
|
DCS_ENTRY = 0x90 |
||||||
|
CSI_ENTRY = 0x9B |
||||||
|
OSC_STRING = 0x9D |
||||||
|
ANSI_PARAMETER_SEP = ";" |
||||||
|
ANSI_CMD_G0 = '(' |
||||||
|
ANSI_CMD_G1 = ')' |
||||||
|
ANSI_CMD_G2 = '*' |
||||||
|
ANSI_CMD_G3 = '+' |
||||||
|
ANSI_CMD_DECPNM = '>' |
||||||
|
ANSI_CMD_DECPAM = '=' |
||||||
|
ANSI_CMD_OSC = ']' |
||||||
|
ANSI_CMD_STR_TERM = '\\' |
||||||
|
|
||||||
|
KEY_CONTROL_PARAM_2 = ";2" |
||||||
|
KEY_CONTROL_PARAM_3 = ";3" |
||||||
|
KEY_CONTROL_PARAM_4 = ";4" |
||||||
|
KEY_CONTROL_PARAM_5 = ";5" |
||||||
|
KEY_CONTROL_PARAM_6 = ";6" |
||||||
|
KEY_CONTROL_PARAM_7 = ";7" |
||||||
|
KEY_CONTROL_PARAM_8 = ";8" |
||||||
|
KEY_ESC_CSI = "\x1B[" |
||||||
|
KEY_ESC_N = "\x1BN" |
||||||
|
KEY_ESC_O = "\x1BO" |
||||||
|
|
||||||
|
FILL_CHARACTER = ' ' |
||||||
|
) |
||||||
|
|
||||||
|
func getByteRange(start byte, end byte) []byte { |
||||||
|
bytes := make([]byte, 0, 32) |
||||||
|
for i := start; i <= end; i++ { |
||||||
|
bytes = append(bytes, byte(i)) |
||||||
|
} |
||||||
|
|
||||||
|
return bytes |
||||||
|
} |
||||||
|
|
||||||
|
var toGroundBytes = getToGroundBytes() |
||||||
|
var executors = getExecuteBytes() |
||||||
|
|
||||||
|
// SPACE 20+A0 hex Always and everywhere a blank space
|
||||||
|
// Intermediate 20-2F hex !"#$%&'()*+,-./
|
||||||
|
var intermeds = getByteRange(0x20, 0x2F) |
||||||
|
|
||||||
|
// Parameters 30-3F hex 0123456789:;<=>?
|
||||||
|
// CSI Parameters 30-39, 3B hex 0123456789;
|
||||||
|
var csiParams = getByteRange(0x30, 0x3F) |
||||||
|
|
||||||
|
var csiCollectables = append(getByteRange(0x30, 0x39), getByteRange(0x3B, 0x3F)...) |
||||||
|
|
||||||
|
// Uppercase 40-5F hex @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
|
||||||
|
var upperCase = getByteRange(0x40, 0x5F) |
||||||
|
|
||||||
|
// Lowercase 60-7E hex `abcdefghijlkmnopqrstuvwxyz{|}~
|
||||||
|
var lowerCase = getByteRange(0x60, 0x7E) |
||||||
|
|
||||||
|
// Alphabetics 40-7E hex (all of upper and lower case)
|
||||||
|
var alphabetics = append(upperCase, lowerCase...) |
||||||
|
|
||||||
|
var printables = getByteRange(0x20, 0x7F) |
||||||
|
|
||||||
|
var escapeIntermediateToGroundBytes = getByteRange(0x30, 0x7E) |
||||||
|
var escapeToGroundBytes = getEscapeToGroundBytes() |
||||||
|
|
||||||
|
// See http://www.vt100.net/emu/vt500_parser.png for description of the complex
|
||||||
|
// byte ranges below
|
||||||
|
|
||||||
|
func getEscapeToGroundBytes() []byte { |
||||||
|
escapeToGroundBytes := getByteRange(0x30, 0x4F) |
||||||
|
escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x51, 0x57)...) |
||||||
|
escapeToGroundBytes = append(escapeToGroundBytes, 0x59) |
||||||
|
escapeToGroundBytes = append(escapeToGroundBytes, 0x5A) |
||||||
|
escapeToGroundBytes = append(escapeToGroundBytes, 0x5C) |
||||||
|
escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x60, 0x7E)...) |
||||||
|
return escapeToGroundBytes |
||||||
|
} |
||||||
|
|
||||||
|
func getExecuteBytes() []byte { |
||||||
|
executeBytes := getByteRange(0x00, 0x17) |
||||||
|
executeBytes = append(executeBytes, 0x19) |
||||||
|
executeBytes = append(executeBytes, getByteRange(0x1C, 0x1F)...) |
||||||
|
return executeBytes |
||||||
|
} |
||||||
|
|
||||||
|
func getToGroundBytes() []byte { |
||||||
|
groundBytes := []byte{0x18} |
||||||
|
groundBytes = append(groundBytes, 0x1A) |
||||||
|
groundBytes = append(groundBytes, getByteRange(0x80, 0x8F)...) |
||||||
|
groundBytes = append(groundBytes, getByteRange(0x91, 0x97)...) |
||||||
|
groundBytes = append(groundBytes, 0x99) |
||||||
|
groundBytes = append(groundBytes, 0x9A) |
||||||
|
groundBytes = append(groundBytes, 0x9C) |
||||||
|
return groundBytes |
||||||
|
} |
||||||
|
|
||||||
|
// Delete 7F hex Always and everywhere ignored
|
||||||
|
// C1 Control 80-9F hex 32 additional control characters
|
||||||
|
// G1 Displayable A1-FE hex 94 additional displayable characters
|
||||||
|
// Special A0+FF hex Same as SPACE and DELETE
|
@ -0,0 +1,7 @@ |
|||||||
|
package ansiterm |
||||||
|
|
||||||
|
type ansiContext struct { |
||||||
|
currentChar byte |
||||||
|
paramBuffer []byte |
||||||
|
interBuffer []byte |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
package ansiterm |
||||||
|
|
||||||
|
type csiEntryState struct { |
||||||
|
baseState |
||||||
|
} |
||||||
|
|
||||||
|
func (csiState csiEntryState) Handle(b byte) (s state, e error) { |
||||||
|
csiState.parser.logf("CsiEntry::Handle %#x", b) |
||||||
|
|
||||||
|
nextState, err := csiState.baseState.Handle(b) |
||||||
|
if nextState != nil || err != nil { |
||||||
|
return nextState, err |
||||||
|
} |
||||||
|
|
||||||
|
switch { |
||||||
|
case sliceContains(alphabetics, b): |
||||||
|
return csiState.parser.ground, nil |
||||||
|
case sliceContains(csiCollectables, b): |
||||||
|
return csiState.parser.csiParam, nil |
||||||
|
case sliceContains(executors, b): |
||||||
|
return csiState, csiState.parser.execute() |
||||||
|
} |
||||||
|
|
||||||
|
return csiState, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (csiState csiEntryState) Transition(s state) error { |
||||||
|
csiState.parser.logf("CsiEntry::Transition %s --> %s", csiState.Name(), s.Name()) |
||||||
|
csiState.baseState.Transition(s) |
||||||
|
|
||||||
|
switch s { |
||||||
|
case csiState.parser.ground: |
||||||
|
return csiState.parser.csiDispatch() |
||||||
|
case csiState.parser.csiParam: |
||||||
|
switch { |
||||||
|
case sliceContains(csiParams, csiState.parser.context.currentChar): |
||||||
|
csiState.parser.collectParam() |
||||||
|
case sliceContains(intermeds, csiState.parser.context.currentChar): |
||||||
|
csiState.parser.collectInter() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (csiState csiEntryState) Enter() error { |
||||||
|
csiState.parser.clear() |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
package ansiterm |
||||||
|
|
||||||
|
type csiParamState struct { |
||||||
|
baseState |
||||||
|
} |
||||||
|
|
||||||
|
func (csiState csiParamState) Handle(b byte) (s state, e error) { |
||||||
|
csiState.parser.logf("CsiParam::Handle %#x", b) |
||||||
|
|
||||||
|
nextState, err := csiState.baseState.Handle(b) |
||||||
|
if nextState != nil || err != nil { |
||||||
|
return nextState, err |
||||||
|
} |
||||||
|
|
||||||
|
switch { |
||||||
|
case sliceContains(alphabetics, b): |
||||||
|
return csiState.parser.ground, nil |
||||||
|
case sliceContains(csiCollectables, b): |
||||||
|
csiState.parser.collectParam() |
||||||
|
return csiState, nil |
||||||
|
case sliceContains(executors, b): |
||||||
|
return csiState, csiState.parser.execute() |
||||||
|
} |
||||||
|
|
||||||
|
return csiState, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (csiState csiParamState) Transition(s state) error { |
||||||
|
csiState.parser.logf("CsiParam::Transition %s --> %s", csiState.Name(), s.Name()) |
||||||
|
csiState.baseState.Transition(s) |
||||||
|
|
||||||
|
switch s { |
||||||
|
case csiState.parser.ground: |
||||||
|
return csiState.parser.csiDispatch() |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
package ansiterm |
||||||
|
|
||||||
|
type escapeIntermediateState struct { |
||||||
|
baseState |
||||||
|
} |
||||||
|
|
||||||
|
func (escState escapeIntermediateState) Handle(b byte) (s state, e error) { |
||||||
|
escState.parser.logf("escapeIntermediateState::Handle %#x", b) |
||||||
|
nextState, err := escState.baseState.Handle(b) |
||||||
|
if nextState != nil || err != nil { |
||||||
|
return nextState, err |
||||||
|
} |
||||||
|
|
||||||
|
switch { |
||||||
|
case sliceContains(intermeds, b): |
||||||
|
return escState, escState.parser.collectInter() |
||||||
|
case sliceContains(executors, b): |
||||||
|
return escState, escState.parser.execute() |
||||||
|
case sliceContains(escapeIntermediateToGroundBytes, b): |
||||||
|
return escState.parser.ground, nil |
||||||
|
} |
||||||
|
|
||||||
|
return escState, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (escState escapeIntermediateState) Transition(s state) error { |
||||||
|
escState.parser.logf("escapeIntermediateState::Transition %s --> %s", escState.Name(), s.Name()) |
||||||
|
escState.baseState.Transition(s) |
||||||
|
|
||||||
|
switch s { |
||||||
|
case escState.parser.ground: |
||||||
|
return escState.parser.escDispatch() |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
package ansiterm |
||||||
|
|
||||||
|
type escapeState struct { |
||||||
|
baseState |
||||||
|
} |
||||||
|
|
||||||
|
func (escState escapeState) Handle(b byte) (s state, e error) { |
||||||
|
escState.parser.logf("escapeState::Handle %#x", b) |
||||||
|
nextState, err := escState.baseState.Handle(b) |
||||||
|
if nextState != nil || err != nil { |
||||||
|
return nextState, err |
||||||
|
} |
||||||
|
|
||||||
|
switch { |
||||||
|
case b == ANSI_ESCAPE_SECONDARY: |
||||||
|
return escState.parser.csiEntry, nil |
||||||
|
case b == ANSI_OSC_STRING_ENTRY: |
||||||
|
return escState.parser.oscString, nil |
||||||
|
case sliceContains(executors, b): |
||||||
|
return escState, escState.parser.execute() |
||||||
|
case sliceContains(escapeToGroundBytes, b): |
||||||
|
return escState.parser.ground, nil |
||||||
|
case sliceContains(intermeds, b): |
||||||
|
return escState.parser.escapeIntermediate, nil |
||||||
|
} |
||||||
|
|
||||||
|
return escState, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (escState escapeState) Transition(s state) error { |
||||||
|
escState.parser.logf("Escape::Transition %s --> %s", escState.Name(), s.Name()) |
||||||
|
escState.baseState.Transition(s) |
||||||
|
|
||||||
|
switch s { |
||||||
|
case escState.parser.ground: |
||||||
|
return escState.parser.escDispatch() |
||||||
|
case escState.parser.escapeIntermediate: |
||||||
|
return escState.parser.collectInter() |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (escState escapeState) Enter() error { |
||||||
|
escState.parser.clear() |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,90 @@ |
|||||||
|
package ansiterm |
||||||
|
|
||||||
|
type AnsiEventHandler interface { |
||||||
|
// Print
|
||||||
|
Print(b byte) error |
||||||
|
|
||||||
|
// Execute C0 commands
|
||||||
|
Execute(b byte) error |
||||||
|
|
||||||
|
// CUrsor Up
|
||||||
|
CUU(int) error |
||||||
|
|
||||||
|
// CUrsor Down
|
||||||
|
CUD(int) error |
||||||
|
|
||||||
|
// CUrsor Forward
|
||||||
|
CUF(int) error |
||||||
|
|
||||||
|
// CUrsor Backward
|
||||||
|
CUB(int) error |
||||||
|
|
||||||
|
// Cursor to Next Line
|
||||||
|
CNL(int) error |
||||||
|
|
||||||
|
// Cursor to Previous Line
|
||||||
|
CPL(int) error |
||||||
|
|
||||||
|
// Cursor Horizontal position Absolute
|
||||||
|
CHA(int) error |
||||||
|
|
||||||
|
// Vertical line Position Absolute
|
||||||
|
VPA(int) error |
||||||
|
|
||||||
|
// CUrsor Position
|
||||||
|
CUP(int, int) error |
||||||
|
|
||||||
|
// Horizontal and Vertical Position (depends on PUM)
|
||||||
|
HVP(int, int) error |
||||||
|
|
||||||
|
// Text Cursor Enable Mode
|
||||||
|
DECTCEM(bool) error |
||||||
|
|
||||||
|
// Origin Mode
|
||||||
|
DECOM(bool) error |
||||||
|
|
||||||
|
// 132 Column Mode
|
||||||
|
DECCOLM(bool) error |
||||||
|
|
||||||
|
// Erase in Display
|
||||||
|
ED(int) error |
||||||
|
|
||||||
|
// Erase in Line
|
||||||
|
EL(int) error |
||||||
|
|
||||||
|
// Insert Line
|
||||||
|
IL(int) error |
||||||
|
|
||||||
|
// Delete Line
|
||||||
|
DL(int) error |
||||||
|
|
||||||
|
// Insert Character
|
||||||
|
ICH(int) error |
||||||
|
|
||||||
|
// Delete Character
|
||||||
|
DCH(int) error |
||||||
|
|
||||||
|
// Set Graphics Rendition
|
||||||
|
SGR([]int) error |
||||||
|
|
||||||
|
// Pan Down
|
||||||
|
SU(int) error |
||||||
|
|
||||||
|
// Pan Up
|
||||||
|
SD(int) error |
||||||
|
|
||||||
|
// Device Attributes
|
||||||
|
DA([]string) error |
||||||
|
|
||||||
|
// Set Top and Bottom Margins
|
||||||
|
DECSTBM(int, int) error |
||||||
|
|
||||||
|
// Index
|
||||||
|
IND() error |
||||||
|
|
||||||
|
// Reverse Index
|
||||||
|
RI() error |
||||||
|
|
||||||
|
// Flush updates from previous commands
|
||||||
|
Flush() error |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
package ansiterm |
||||||
|
|
||||||
|
type groundState struct { |
||||||
|
baseState |
||||||
|
} |
||||||
|
|
||||||
|
func (gs groundState) Handle(b byte) (s state, e error) { |
||||||
|
gs.parser.context.currentChar = b |
||||||
|
|
||||||
|
nextState, err := gs.baseState.Handle(b) |
||||||
|
if nextState != nil || err != nil { |
||||||
|
return nextState, err |
||||||
|
} |
||||||
|
|
||||||
|
switch { |
||||||
|
case sliceContains(printables, b): |
||||||
|
return gs, gs.parser.print() |
||||||
|
|
||||||
|
case sliceContains(executors, b): |
||||||
|
return gs, gs.parser.execute() |
||||||
|
} |
||||||
|
|
||||||
|
return gs, nil |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
package ansiterm |
||||||
|
|
||||||
|
type oscStringState struct { |
||||||
|
baseState |
||||||
|
} |
||||||
|
|
||||||
|
func (oscState oscStringState) Handle(b byte) (s state, e error) { |
||||||
|
oscState.parser.logf("OscString::Handle %#x", b) |
||||||
|
nextState, err := oscState.baseState.Handle(b) |
||||||
|
if nextState != nil || err != nil { |
||||||
|
return nextState, err |
||||||
|
} |
||||||
|
|
||||||
|
switch { |
||||||
|
case isOscStringTerminator(b): |
||||||
|
return oscState.parser.ground, nil |
||||||
|
} |
||||||
|
|
||||||
|
return oscState, nil |
||||||
|
} |
||||||
|
|
||||||
|
// See below for OSC string terminators for linux
|
||||||
|
// http://man7.org/linux/man-pages/man4/console_codes.4.html
|
||||||
|
func isOscStringTerminator(b byte) bool { |
||||||
|
|
||||||
|
if b == ANSI_BEL || b == 0x5C { |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
} |
@ -0,0 +1,151 @@ |
|||||||
|
package ansiterm |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"log" |
||||||
|
"os" |
||||||
|
) |
||||||
|
|
||||||
|
type AnsiParser struct { |
||||||
|
currState state |
||||||
|
eventHandler AnsiEventHandler |
||||||
|
context *ansiContext |
||||||
|
csiEntry state |
||||||
|
csiParam state |
||||||
|
dcsEntry state |
||||||
|
escape state |
||||||
|
escapeIntermediate state |
||||||
|
error state |
||||||
|
ground state |
||||||
|
oscString state |
||||||
|
stateMap []state |
||||||
|
|
||||||
|
logf func(string, ...interface{}) |
||||||
|
} |
||||||
|
|
||||||
|
type Option func(*AnsiParser) |
||||||
|
|
||||||
|
func WithLogf(f func(string, ...interface{})) Option { |
||||||
|
return func(ap *AnsiParser) { |
||||||
|
ap.logf = f |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func CreateParser(initialState string, evtHandler AnsiEventHandler, opts ...Option) *AnsiParser { |
||||||
|
ap := &AnsiParser{ |
||||||
|
eventHandler: evtHandler, |
||||||
|
context: &ansiContext{}, |
||||||
|
} |
||||||
|
for _, o := range opts { |
||||||
|
o(ap) |
||||||
|
} |
||||||
|
|
||||||
|
if isDebugEnv := os.Getenv(LogEnv); isDebugEnv == "1" { |
||||||
|
logFile, _ := os.Create("ansiParser.log") |
||||||
|
logger := log.New(logFile, "", log.LstdFlags) |
||||||
|
if ap.logf != nil { |
||||||
|
l := ap.logf |
||||||
|
ap.logf = func(s string, v ...interface{}) { |
||||||
|
l(s, v...) |
||||||
|
logger.Printf(s, v...) |
||||||
|
} |
||||||
|
} else { |
||||||
|
ap.logf = logger.Printf |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if ap.logf == nil { |
||||||
|
ap.logf = func(string, ...interface{}) {} |
||||||
|
} |
||||||
|
|
||||||
|
ap.csiEntry = csiEntryState{baseState{name: "CsiEntry", parser: ap}} |
||||||
|
ap.csiParam = csiParamState{baseState{name: "CsiParam", parser: ap}} |
||||||
|
ap.dcsEntry = dcsEntryState{baseState{name: "DcsEntry", parser: ap}} |
||||||
|
ap.escape = escapeState{baseState{name: "Escape", parser: ap}} |
||||||
|
ap.escapeIntermediate = escapeIntermediateState{baseState{name: "EscapeIntermediate", parser: ap}} |
||||||
|
ap.error = errorState{baseState{name: "Error", parser: ap}} |
||||||
|
ap.ground = groundState{baseState{name: "Ground", parser: ap}} |
||||||
|
ap.oscString = oscStringState{baseState{name: "OscString", parser: ap}} |
||||||
|
|
||||||
|
ap.stateMap = []state{ |
||||||
|
ap.csiEntry, |
||||||
|
ap.csiParam, |
||||||
|
ap.dcsEntry, |
||||||
|
ap.escape, |
||||||
|
ap.escapeIntermediate, |
||||||
|
ap.error, |
||||||
|
ap.ground, |
||||||
|
ap.oscString, |
||||||
|
} |
||||||
|
|
||||||
|
ap.currState = getState(initialState, ap.stateMap) |
||||||
|
|
||||||
|
ap.logf("CreateParser: parser %p", ap) |
||||||
|
return ap |
||||||
|
} |
||||||
|
|
||||||
|
func getState(name string, states []state) state { |
||||||
|
for _, el := range states { |
||||||
|
if el.Name() == name { |
||||||
|
return el |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (ap *AnsiParser) Parse(bytes []byte) (int, error) { |
||||||
|
for i, b := range bytes { |
||||||
|
if err := ap.handle(b); err != nil { |
||||||
|
return i, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return len(bytes), ap.eventHandler.Flush() |
||||||
|
} |
||||||
|
|
||||||
|
func (ap *AnsiParser) handle(b byte) error { |
||||||
|
ap.context.currentChar = b |
||||||
|
newState, err := ap.currState.Handle(b) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if newState == nil { |
||||||
|
ap.logf("WARNING: newState is nil") |
||||||
|
return errors.New("New state of 'nil' is invalid.") |
||||||
|
} |
||||||
|
|
||||||
|
if newState != ap.currState { |
||||||
|
if err := ap.changeState(newState); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (ap *AnsiParser) changeState(newState state) error { |
||||||
|
ap.logf("ChangeState %s --> %s", ap.currState.Name(), newState.Name()) |
||||||
|
|
||||||
|
// Exit old state
|
||||||
|
if err := ap.currState.Exit(); err != nil { |
||||||
|
ap.logf("Exit state '%s' failed with : '%v'", ap.currState.Name(), err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Perform transition action
|
||||||
|
if err := ap.currState.Transition(newState); err != nil { |
||||||
|
ap.logf("Transition from '%s' to '%s' failed with: '%v'", ap.currState.Name(), newState.Name, err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Enter new state
|
||||||
|
if err := newState.Enter(); err != nil { |
||||||
|
ap.logf("Enter state '%s' failed with: '%v'", newState.Name(), err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
ap.currState = newState |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,99 @@ |
|||||||
|
package ansiterm |
||||||
|
|
||||||
|
import ( |
||||||
|
"strconv" |
||||||
|
) |
||||||
|
|
||||||
|
func parseParams(bytes []byte) ([]string, error) { |
||||||
|
paramBuff := make([]byte, 0, 0) |
||||||
|
params := []string{} |
||||||
|
|
||||||
|
for _, v := range bytes { |
||||||
|
if v == ';' { |
||||||
|
if len(paramBuff) > 0 { |
||||||
|
// Completed parameter, append it to the list
|
||||||
|
s := string(paramBuff) |
||||||
|
params = append(params, s) |
||||||
|
paramBuff = make([]byte, 0, 0) |
||||||
|
} |
||||||
|
} else { |
||||||
|
paramBuff = append(paramBuff, v) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Last parameter may not be terminated with ';'
|
||||||
|
if len(paramBuff) > 0 { |
||||||
|
s := string(paramBuff) |
||||||
|
params = append(params, s) |
||||||
|
} |
||||||
|
|
||||||
|
return params, nil |
||||||
|
} |
||||||
|
|
||||||
|
func parseCmd(context ansiContext) (string, error) { |
||||||
|
return string(context.currentChar), nil |
||||||
|
} |
||||||
|
|
||||||
|
func getInt(params []string, dflt int) int { |
||||||
|
i := getInts(params, 1, dflt)[0] |
||||||
|
return i |
||||||
|
} |
||||||
|
|
||||||
|
func getInts(params []string, minCount int, dflt int) []int { |
||||||
|
ints := []int{} |
||||||
|
|
||||||
|
for _, v := range params { |
||||||
|
i, _ := strconv.Atoi(v) |
||||||
|
// Zero is mapped to the default value in VT100.
|
||||||
|
if i == 0 { |
||||||
|
i = dflt |
||||||
|
} |
||||||
|
ints = append(ints, i) |
||||||
|
} |
||||||
|
|
||||||
|
if len(ints) < minCount { |
||||||
|
remaining := minCount - len(ints) |
||||||
|
for i := 0; i < remaining; i++ { |
||||||
|
ints = append(ints, dflt) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ints |
||||||
|
} |
||||||
|
|
||||||
|
func (ap *AnsiParser) modeDispatch(param string, set bool) error { |
||||||
|
switch param { |
||||||
|
case "?3": |
||||||
|
return ap.eventHandler.DECCOLM(set) |
||||||
|
case "?6": |
||||||
|
return ap.eventHandler.DECOM(set) |
||||||
|
case "?25": |
||||||
|
return ap.eventHandler.DECTCEM(set) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (ap *AnsiParser) hDispatch(params []string) error { |
||||||
|
if len(params) == 1 { |
||||||
|
return ap.modeDispatch(params[0], true) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (ap *AnsiParser) lDispatch(params []string) error { |
||||||
|
if len(params) == 1 { |
||||||
|
return ap.modeDispatch(params[0], false) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func getEraseParam(params []string) int { |
||||||
|
param := getInt(params, 0) |
||||||
|
if param < 0 || 3 < param { |
||||||
|
param = 0 |
||||||
|
} |
||||||
|
|
||||||
|
return param |
||||||
|
} |
@ -0,0 +1,119 @@ |
|||||||
|
package ansiterm |
||||||
|
|
||||||
|
func (ap *AnsiParser) collectParam() error { |
||||||
|
currChar := ap.context.currentChar |
||||||
|
ap.logf("collectParam %#x", currChar) |
||||||
|
ap.context.paramBuffer = append(ap.context.paramBuffer, currChar) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (ap *AnsiParser) collectInter() error { |
||||||
|
currChar := ap.context.currentChar |
||||||
|
ap.logf("collectInter %#x", currChar) |
||||||
|
ap.context.paramBuffer = append(ap.context.interBuffer, currChar) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (ap *AnsiParser) escDispatch() error { |
||||||
|
cmd, _ := parseCmd(*ap.context) |
||||||
|
intermeds := ap.context.interBuffer |
||||||
|
ap.logf("escDispatch currentChar: %#x", ap.context.currentChar) |
||||||
|
ap.logf("escDispatch: %v(%v)", cmd, intermeds) |
||||||
|
|
||||||
|
switch cmd { |
||||||
|
case "D": // IND
|
||||||
|
return ap.eventHandler.IND() |
||||||
|
case "E": // NEL, equivalent to CRLF
|
||||||
|
err := ap.eventHandler.Execute(ANSI_CARRIAGE_RETURN) |
||||||
|
if err == nil { |
||||||
|
err = ap.eventHandler.Execute(ANSI_LINE_FEED) |
||||||
|
} |
||||||
|
return err |
||||||
|
case "M": // RI
|
||||||
|
return ap.eventHandler.RI() |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (ap *AnsiParser) csiDispatch() error { |
||||||
|
cmd, _ := parseCmd(*ap.context) |
||||||
|
params, _ := parseParams(ap.context.paramBuffer) |
||||||
|
ap.logf("Parsed params: %v with length: %d", params, len(params)) |
||||||
|
|
||||||
|
ap.logf("csiDispatch: %v(%v)", cmd, params) |
||||||
|
|
||||||
|
switch cmd { |
||||||
|
case "@": |
||||||
|
return ap.eventHandler.ICH(getInt(params, 1)) |
||||||
|
case "A": |
||||||
|
return ap.eventHandler.CUU(getInt(params, 1)) |
||||||
|
case "B": |
||||||
|
return ap.eventHandler.CUD(getInt(params, 1)) |
||||||
|
case "C": |
||||||
|
return ap.eventHandler.CUF(getInt(params, 1)) |
||||||
|
case "D": |
||||||
|
return ap.eventHandler.CUB(getInt(params, 1)) |
||||||
|
case "E": |
||||||
|
return ap.eventHandler.CNL(getInt(params, 1)) |
||||||
|
case "F": |
||||||
|
return ap.eventHandler.CPL(getInt(params, 1)) |
||||||
|
case "G": |
||||||
|
return ap.eventHandler.CHA(getInt(params, 1)) |
||||||
|
case "H": |
||||||
|
ints := getInts(params, 2, 1) |
||||||
|
x, y := ints[0], ints[1] |
||||||
|
return ap.eventHandler.CUP(x, y) |
||||||
|
case "J": |
||||||
|
param := getEraseParam(params) |
||||||
|
return ap.eventHandler.ED(param) |
||||||
|
case "K": |
||||||
|
param := getEraseParam(params) |
||||||
|
return ap.eventHandler.EL(param) |
||||||
|
case "L": |
||||||
|
return ap.eventHandler.IL(getInt(params, 1)) |
||||||
|
case "M": |
||||||
|
return ap.eventHandler.DL(getInt(params, 1)) |
||||||
|
case "P": |
||||||
|
return ap.eventHandler.DCH(getInt(params, 1)) |
||||||
|
case "S": |
||||||
|
return ap.eventHandler.SU(getInt(params, 1)) |
||||||
|
case "T": |
||||||
|
return ap.eventHandler.SD(getInt(params, 1)) |
||||||
|
case "c": |
||||||
|
return ap.eventHandler.DA(params) |
||||||
|
case "d": |
||||||
|
return ap.eventHandler.VPA(getInt(params, 1)) |
||||||
|
case "f": |
||||||
|
ints := getInts(params, 2, 1) |
||||||
|
x, y := ints[0], ints[1] |
||||||
|
return ap.eventHandler.HVP(x, y) |
||||||
|
case "h": |
||||||
|
return ap.hDispatch(params) |
||||||
|
case "l": |
||||||
|
return ap.lDispatch(params) |
||||||
|
case "m": |
||||||
|
return ap.eventHandler.SGR(getInts(params, 1, 0)) |
||||||
|
case "r": |
||||||
|
ints := getInts(params, 2, 1) |
||||||
|
top, bottom := ints[0], ints[1] |
||||||
|
return ap.eventHandler.DECSTBM(top, bottom) |
||||||
|
default: |
||||||
|
ap.logf("ERROR: Unsupported CSI command: '%s', with full context: %v", cmd, ap.context) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func (ap *AnsiParser) print() error { |
||||||
|
return ap.eventHandler.Print(ap.context.currentChar) |
||||||
|
} |
||||||
|
|
||||||
|
func (ap *AnsiParser) clear() error { |
||||||
|
ap.context = &ansiContext{} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (ap *AnsiParser) execute() error { |
||||||
|
return ap.eventHandler.Execute(ap.context.currentChar) |
||||||
|
} |
@ -0,0 +1,71 @@ |
|||||||
|
package ansiterm |
||||||
|
|
||||||
|
type stateID int |
||||||
|
|
||||||
|
type state interface { |
||||||
|
Enter() error |
||||||
|
Exit() error |
||||||
|
Handle(byte) (state, error) |
||||||
|
Name() string |
||||||
|
Transition(state) error |
||||||
|
} |
||||||
|
|
||||||
|
type baseState struct { |
||||||
|
name string |
||||||
|
parser *AnsiParser |
||||||
|
} |
||||||
|
|
||||||
|
func (base baseState) Enter() error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (base baseState) Exit() error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (base baseState) Handle(b byte) (s state, e error) { |
||||||
|
|
||||||
|
switch { |
||||||
|
case b == CSI_ENTRY: |
||||||
|
return base.parser.csiEntry, nil |
||||||
|
case b == DCS_ENTRY: |
||||||
|
return base.parser.dcsEntry, nil |
||||||
|
case b == ANSI_ESCAPE_PRIMARY: |
||||||
|
return base.parser.escape, nil |
||||||
|
case b == OSC_STRING: |
||||||
|
return base.parser.oscString, nil |
||||||
|
case sliceContains(toGroundBytes, b): |
||||||
|
return base.parser.ground, nil |
||||||
|
} |
||||||
|
|
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (base baseState) Name() string { |
||||||
|
return base.name |
||||||
|
} |
||||||
|
|
||||||
|
func (base baseState) Transition(s state) error { |
||||||
|
if s == base.parser.ground { |
||||||
|
execBytes := []byte{0x18} |
||||||
|
execBytes = append(execBytes, 0x1A) |
||||||
|
execBytes = append(execBytes, getByteRange(0x80, 0x8F)...) |
||||||
|
execBytes = append(execBytes, getByteRange(0x91, 0x97)...) |
||||||
|
execBytes = append(execBytes, 0x99) |
||||||
|
execBytes = append(execBytes, 0x9A) |
||||||
|
|
||||||
|
if sliceContains(execBytes, base.parser.context.currentChar) { |
||||||
|
return base.parser.execute() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
type dcsEntryState struct { |
||||||
|
baseState |
||||||
|
} |
||||||
|
|
||||||
|
type errorState struct { |
||||||
|
baseState |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
package ansiterm |
||||||
|
|
||||||
|
import ( |
||||||
|
"strconv" |
||||||
|
) |
||||||
|
|
||||||
|
func sliceContains(bytes []byte, b byte) bool { |
||||||
|
for _, v := range bytes { |
||||||
|
if v == b { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
func convertBytesToInteger(bytes []byte) int { |
||||||
|
s := string(bytes) |
||||||
|
i, _ := strconv.Atoi(s) |
||||||
|
return i |
||||||
|
} |
@ -0,0 +1,182 @@ |
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"syscall" |
||||||
|
|
||||||
|
"github.com/Azure/go-ansiterm" |
||||||
|
) |
||||||
|
|
||||||
|
// Windows keyboard constants
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx.
|
||||||
|
const ( |
||||||
|
VK_PRIOR = 0x21 // PAGE UP key
|
||||||
|
VK_NEXT = 0x22 // PAGE DOWN key
|
||||||
|
VK_END = 0x23 // END key
|
||||||
|
VK_HOME = 0x24 // HOME key
|
||||||
|
VK_LEFT = 0x25 // LEFT ARROW key
|
||||||
|
VK_UP = 0x26 // UP ARROW key
|
||||||
|
VK_RIGHT = 0x27 // RIGHT ARROW key
|
||||||
|
VK_DOWN = 0x28 // DOWN ARROW key
|
||||||
|
VK_SELECT = 0x29 // SELECT key
|
||||||
|
VK_PRINT = 0x2A // PRINT key
|
||||||
|
VK_EXECUTE = 0x2B // EXECUTE key
|
||||||
|
VK_SNAPSHOT = 0x2C // PRINT SCREEN key
|
||||||
|
VK_INSERT = 0x2D // INS key
|
||||||
|
VK_DELETE = 0x2E // DEL key
|
||||||
|
VK_HELP = 0x2F // HELP key
|
||||||
|
VK_F1 = 0x70 // F1 key
|
||||||
|
VK_F2 = 0x71 // F2 key
|
||||||
|
VK_F3 = 0x72 // F3 key
|
||||||
|
VK_F4 = 0x73 // F4 key
|
||||||
|
VK_F5 = 0x74 // F5 key
|
||||||
|
VK_F6 = 0x75 // F6 key
|
||||||
|
VK_F7 = 0x76 // F7 key
|
||||||
|
VK_F8 = 0x77 // F8 key
|
||||||
|
VK_F9 = 0x78 // F9 key
|
||||||
|
VK_F10 = 0x79 // F10 key
|
||||||
|
VK_F11 = 0x7A // F11 key
|
||||||
|
VK_F12 = 0x7B // F12 key
|
||||||
|
|
||||||
|
RIGHT_ALT_PRESSED = 0x0001 |
||||||
|
LEFT_ALT_PRESSED = 0x0002 |
||||||
|
RIGHT_CTRL_PRESSED = 0x0004 |
||||||
|
LEFT_CTRL_PRESSED = 0x0008 |
||||||
|
SHIFT_PRESSED = 0x0010 |
||||||
|
NUMLOCK_ON = 0x0020 |
||||||
|
SCROLLLOCK_ON = 0x0040 |
||||||
|
CAPSLOCK_ON = 0x0080 |
||||||
|
ENHANCED_KEY = 0x0100 |
||||||
|
) |
||||||
|
|
||||||
|
type ansiCommand struct { |
||||||
|
CommandBytes []byte |
||||||
|
Command string |
||||||
|
Parameters []string |
||||||
|
IsSpecial bool |
||||||
|
} |
||||||
|
|
||||||
|
func newAnsiCommand(command []byte) *ansiCommand { |
||||||
|
|
||||||
|
if isCharacterSelectionCmdChar(command[1]) { |
||||||
|
// Is Character Set Selection commands
|
||||||
|
return &ansiCommand{ |
||||||
|
CommandBytes: command, |
||||||
|
Command: string(command), |
||||||
|
IsSpecial: true, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// last char is command character
|
||||||
|
lastCharIndex := len(command) - 1 |
||||||
|
|
||||||
|
ac := &ansiCommand{ |
||||||
|
CommandBytes: command, |
||||||
|
Command: string(command[lastCharIndex]), |
||||||
|
IsSpecial: false, |
||||||
|
} |
||||||
|
|
||||||
|
// more than a single escape
|
||||||
|
if lastCharIndex != 0 { |
||||||
|
start := 1 |
||||||
|
// skip if double char escape sequence
|
||||||
|
if command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_ESCAPE_SECONDARY { |
||||||
|
start++ |
||||||
|
} |
||||||
|
// convert this to GetNextParam method
|
||||||
|
ac.Parameters = strings.Split(string(command[start:lastCharIndex]), ansiterm.ANSI_PARAMETER_SEP) |
||||||
|
} |
||||||
|
|
||||||
|
return ac |
||||||
|
} |
||||||
|
|
||||||
|
func (ac *ansiCommand) paramAsSHORT(index int, defaultValue int16) int16 { |
||||||
|
if index < 0 || index >= len(ac.Parameters) { |
||||||
|
return defaultValue |
||||||
|
} |
||||||
|
|
||||||
|
param, err := strconv.ParseInt(ac.Parameters[index], 10, 16) |
||||||
|
if err != nil { |
||||||
|
return defaultValue |
||||||
|
} |
||||||
|
|
||||||
|
return int16(param) |
||||||
|
} |
||||||
|
|
||||||
|
func (ac *ansiCommand) String() string { |
||||||
|
return fmt.Sprintf("0x%v \"%v\" (\"%v\")", |
||||||
|
bytesToHex(ac.CommandBytes), |
||||||
|
ac.Command, |
||||||
|
strings.Join(ac.Parameters, "\",\"")) |
||||||
|
} |
||||||
|
|
||||||
|
// isAnsiCommandChar returns true if the passed byte falls within the range of ANSI commands.
|
||||||
|
// See http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html.
|
||||||
|
func isAnsiCommandChar(b byte) bool { |
||||||
|
switch { |
||||||
|
case ansiterm.ANSI_COMMAND_FIRST <= b && b <= ansiterm.ANSI_COMMAND_LAST && b != ansiterm.ANSI_ESCAPE_SECONDARY: |
||||||
|
return true |
||||||
|
case b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_OSC || b == ansiterm.ANSI_CMD_DECPAM || b == ansiterm.ANSI_CMD_DECPNM: |
||||||
|
// non-CSI escape sequence terminator
|
||||||
|
return true |
||||||
|
case b == ansiterm.ANSI_CMD_STR_TERM || b == ansiterm.ANSI_BEL: |
||||||
|
// String escape sequence terminator
|
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
func isXtermOscSequence(command []byte, current byte) bool { |
||||||
|
return (len(command) >= 2 && command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_CMD_OSC && current != ansiterm.ANSI_BEL) |
||||||
|
} |
||||||
|
|
||||||
|
func isCharacterSelectionCmdChar(b byte) bool { |
||||||
|
return (b == ansiterm.ANSI_CMD_G0 || b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_G2 || b == ansiterm.ANSI_CMD_G3) |
||||||
|
} |
||||||
|
|
||||||
|
// bytesToHex converts a slice of bytes to a human-readable string.
|
||||||
|
func bytesToHex(b []byte) string { |
||||||
|
hex := make([]string, len(b)) |
||||||
|
for i, ch := range b { |
||||||
|
hex[i] = fmt.Sprintf("%X", ch) |
||||||
|
} |
||||||
|
return strings.Join(hex, "") |
||||||
|
} |
||||||
|
|
||||||
|
// ensureInRange adjusts the passed value, if necessary, to ensure it is within
|
||||||
|
// the passed min / max range.
|
||||||
|
func ensureInRange(n int16, min int16, max int16) int16 { |
||||||
|
if n < min { |
||||||
|
return min |
||||||
|
} else if n > max { |
||||||
|
return max |
||||||
|
} else { |
||||||
|
return n |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func GetStdFile(nFile int) (*os.File, uintptr) { |
||||||
|
var file *os.File |
||||||
|
switch nFile { |
||||||
|
case syscall.STD_INPUT_HANDLE: |
||||||
|
file = os.Stdin |
||||||
|
case syscall.STD_OUTPUT_HANDLE: |
||||||
|
file = os.Stdout |
||||||
|
case syscall.STD_ERROR_HANDLE: |
||||||
|
file = os.Stderr |
||||||
|
default: |
||||||
|
panic(fmt.Errorf("Invalid standard handle identifier: %v", nFile)) |
||||||
|
} |
||||||
|
|
||||||
|
fd, err := syscall.GetStdHandle(nFile) |
||||||
|
if err != nil { |
||||||
|
panic(fmt.Errorf("Invalid standard handle identifier: %v -- %v", nFile, err)) |
||||||
|
} |
||||||
|
|
||||||
|
return file, uintptr(fd) |
||||||
|
} |
@ -0,0 +1,327 @@ |
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"syscall" |
||||||
|
"unsafe" |
||||||
|
) |
||||||
|
|
||||||
|
//===========================================================================================================
|
||||||
|
// IMPORTANT NOTE:
|
||||||
|
//
|
||||||
|
// The methods below make extensive use of the "unsafe" package to obtain the required pointers.
|
||||||
|
// Beginning in Go 1.3, the garbage collector may release local variables (e.g., incoming arguments, stack
|
||||||
|
// variables) the pointers reference *before* the API completes.
|
||||||
|
//
|
||||||
|
// As a result, in those cases, the code must hint that the variables remain in active by invoking the
|
||||||
|
// dummy method "use" (see below). Newer versions of Go are planned to change the mechanism to no longer
|
||||||
|
// require unsafe pointers.
|
||||||
|
//
|
||||||
|
// If you add or modify methods, ENSURE protection of local variables through the "use" builtin to inform
|
||||||
|
// the garbage collector the variables remain in use if:
|
||||||
|
//
|
||||||
|
// -- The value is not a pointer (e.g., int32, struct)
|
||||||
|
// -- The value is not referenced by the method after passing the pointer to Windows
|
||||||
|
//
|
||||||
|
// See http://golang.org/doc/go1.3.
|
||||||
|
//===========================================================================================================
|
||||||
|
|
||||||
|
var ( |
||||||
|
kernel32DLL = syscall.NewLazyDLL("kernel32.dll") |
||||||
|
|
||||||
|
getConsoleCursorInfoProc = kernel32DLL.NewProc("GetConsoleCursorInfo") |
||||||
|
setConsoleCursorInfoProc = kernel32DLL.NewProc("SetConsoleCursorInfo") |
||||||
|
setConsoleCursorPositionProc = kernel32DLL.NewProc("SetConsoleCursorPosition") |
||||||
|
setConsoleModeProc = kernel32DLL.NewProc("SetConsoleMode") |
||||||
|
getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo") |
||||||
|
setConsoleScreenBufferSizeProc = kernel32DLL.NewProc("SetConsoleScreenBufferSize") |
||||||
|
scrollConsoleScreenBufferProc = kernel32DLL.NewProc("ScrollConsoleScreenBufferA") |
||||||
|
setConsoleTextAttributeProc = kernel32DLL.NewProc("SetConsoleTextAttribute") |
||||||
|
setConsoleWindowInfoProc = kernel32DLL.NewProc("SetConsoleWindowInfo") |
||||||
|
writeConsoleOutputProc = kernel32DLL.NewProc("WriteConsoleOutputW") |
||||||
|
readConsoleInputProc = kernel32DLL.NewProc("ReadConsoleInputW") |
||||||
|
waitForSingleObjectProc = kernel32DLL.NewProc("WaitForSingleObject") |
||||||
|
) |
||||||
|
|
||||||
|
// Windows Console constants
|
||||||
|
const ( |
||||||
|
// Console modes
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
|
||||||
|
ENABLE_PROCESSED_INPUT = 0x0001 |
||||||
|
ENABLE_LINE_INPUT = 0x0002 |
||||||
|
ENABLE_ECHO_INPUT = 0x0004 |
||||||
|
ENABLE_WINDOW_INPUT = 0x0008 |
||||||
|
ENABLE_MOUSE_INPUT = 0x0010 |
||||||
|
ENABLE_INSERT_MODE = 0x0020 |
||||||
|
ENABLE_QUICK_EDIT_MODE = 0x0040 |
||||||
|
ENABLE_EXTENDED_FLAGS = 0x0080 |
||||||
|
ENABLE_AUTO_POSITION = 0x0100 |
||||||
|
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 |
||||||
|
|
||||||
|
ENABLE_PROCESSED_OUTPUT = 0x0001 |
||||||
|
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002 |
||||||
|
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 |
||||||
|
DISABLE_NEWLINE_AUTO_RETURN = 0x0008 |
||||||
|
ENABLE_LVB_GRID_WORLDWIDE = 0x0010 |
||||||
|
|
||||||
|
// Character attributes
|
||||||
|
// Note:
|
||||||
|
// -- The attributes are combined to produce various colors (e.g., Blue + Green will create Cyan).
|
||||||
|
// Clearing all foreground or background colors results in black; setting all creates white.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682088(v=vs.85).aspx#_win32_character_attributes.
|
||||||
|
FOREGROUND_BLUE uint16 = 0x0001 |
||||||
|
FOREGROUND_GREEN uint16 = 0x0002 |
||||||
|
FOREGROUND_RED uint16 = 0x0004 |
||||||
|
FOREGROUND_INTENSITY uint16 = 0x0008 |
||||||
|
FOREGROUND_MASK uint16 = 0x000F |
||||||
|
|
||||||
|
BACKGROUND_BLUE uint16 = 0x0010 |
||||||
|
BACKGROUND_GREEN uint16 = 0x0020 |
||||||
|
BACKGROUND_RED uint16 = 0x0040 |
||||||
|
BACKGROUND_INTENSITY uint16 = 0x0080 |
||||||
|
BACKGROUND_MASK uint16 = 0x00F0 |
||||||
|
|
||||||
|
COMMON_LVB_MASK uint16 = 0xFF00 |
||||||
|
COMMON_LVB_REVERSE_VIDEO uint16 = 0x4000 |
||||||
|
COMMON_LVB_UNDERSCORE uint16 = 0x8000 |
||||||
|
|
||||||
|
// Input event types
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
|
||||||
|
KEY_EVENT = 0x0001 |
||||||
|
MOUSE_EVENT = 0x0002 |
||||||
|
WINDOW_BUFFER_SIZE_EVENT = 0x0004 |
||||||
|
MENU_EVENT = 0x0008 |
||||||
|
FOCUS_EVENT = 0x0010 |
||||||
|
|
||||||
|
// WaitForSingleObject return codes
|
||||||
|
WAIT_ABANDONED = 0x00000080 |
||||||
|
WAIT_FAILED = 0xFFFFFFFF |
||||||
|
WAIT_SIGNALED = 0x0000000 |
||||||
|
WAIT_TIMEOUT = 0x00000102 |
||||||
|
|
||||||
|
// WaitForSingleObject wait duration
|
||||||
|
WAIT_INFINITE = 0xFFFFFFFF |
||||||
|
WAIT_ONE_SECOND = 1000 |
||||||
|
WAIT_HALF_SECOND = 500 |
||||||
|
WAIT_QUARTER_SECOND = 250 |
||||||
|
) |
||||||
|
|
||||||
|
// Windows API Console types
|
||||||
|
// -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682101(v=vs.85).aspx for Console specific types (e.g., COORD)
|
||||||
|
// -- See https://msdn.microsoft.com/en-us/library/aa296569(v=vs.60).aspx for comments on alignment
|
||||||
|
type ( |
||||||
|
CHAR_INFO struct { |
||||||
|
UnicodeChar uint16 |
||||||
|
Attributes uint16 |
||||||
|
} |
||||||
|
|
||||||
|
CONSOLE_CURSOR_INFO struct { |
||||||
|
Size uint32 |
||||||
|
Visible int32 |
||||||
|
} |
||||||
|
|
||||||
|
CONSOLE_SCREEN_BUFFER_INFO struct { |
||||||
|
Size COORD |
||||||
|
CursorPosition COORD |
||||||
|
Attributes uint16 |
||||||
|
Window SMALL_RECT |
||||||
|
MaximumWindowSize COORD |
||||||
|
} |
||||||
|
|
||||||
|
COORD struct { |
||||||
|
X int16 |
||||||
|
Y int16 |
||||||
|
} |
||||||
|
|
||||||
|
SMALL_RECT struct { |
||||||
|
Left int16 |
||||||
|
Top int16 |
||||||
|
Right int16 |
||||||
|
Bottom int16 |
||||||
|
} |
||||||
|
|
||||||
|
// INPUT_RECORD is a C/C++ union of which KEY_EVENT_RECORD is one case, it is also the largest
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
|
||||||
|
INPUT_RECORD struct { |
||||||
|
EventType uint16 |
||||||
|
KeyEvent KEY_EVENT_RECORD |
||||||
|
} |
||||||
|
|
||||||
|
KEY_EVENT_RECORD struct { |
||||||
|
KeyDown int32 |
||||||
|
RepeatCount uint16 |
||||||
|
VirtualKeyCode uint16 |
||||||
|
VirtualScanCode uint16 |
||||||
|
UnicodeChar uint16 |
||||||
|
ControlKeyState uint32 |
||||||
|
} |
||||||
|
|
||||||
|
WINDOW_BUFFER_SIZE struct { |
||||||
|
Size COORD |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
// boolToBOOL converts a Go bool into a Windows int32.
|
||||||
|
func boolToBOOL(f bool) int32 { |
||||||
|
if f { |
||||||
|
return int32(1) |
||||||
|
} else { |
||||||
|
return int32(0) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// GetConsoleCursorInfo retrieves information about the size and visiblity of the console cursor.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683163(v=vs.85).aspx.
|
||||||
|
func GetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error { |
||||||
|
r1, r2, err := getConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0) |
||||||
|
return checkError(r1, r2, err) |
||||||
|
} |
||||||
|
|
||||||
|
// SetConsoleCursorInfo sets the size and visiblity of the console cursor.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686019(v=vs.85).aspx.
|
||||||
|
func SetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error { |
||||||
|
r1, r2, err := setConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0) |
||||||
|
return checkError(r1, r2, err) |
||||||
|
} |
||||||
|
|
||||||
|
// SetConsoleCursorPosition location of the console cursor.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx.
|
||||||
|
func SetConsoleCursorPosition(handle uintptr, coord COORD) error { |
||||||
|
r1, r2, err := setConsoleCursorPositionProc.Call(handle, coordToPointer(coord)) |
||||||
|
use(coord) |
||||||
|
return checkError(r1, r2, err) |
||||||
|
} |
||||||
|
|
||||||
|
// GetConsoleMode gets the console mode for given file descriptor
|
||||||
|
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx.
|
||||||
|
func GetConsoleMode(handle uintptr) (mode uint32, err error) { |
||||||
|
err = syscall.GetConsoleMode(syscall.Handle(handle), &mode) |
||||||
|
return mode, err |
||||||
|
} |
||||||
|
|
||||||
|
// SetConsoleMode sets the console mode for given file descriptor
|
||||||
|
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
|
||||||
|
func SetConsoleMode(handle uintptr, mode uint32) error { |
||||||
|
r1, r2, err := setConsoleModeProc.Call(handle, uintptr(mode), 0) |
||||||
|
use(mode) |
||||||
|
return checkError(r1, r2, err) |
||||||
|
} |
||||||
|
|
||||||
|
// GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer.
|
||||||
|
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx.
|
||||||
|
func GetConsoleScreenBufferInfo(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) { |
||||||
|
info := CONSOLE_SCREEN_BUFFER_INFO{} |
||||||
|
err := checkError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0)) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return &info, nil |
||||||
|
} |
||||||
|
|
||||||
|
func ScrollConsoleScreenBuffer(handle uintptr, scrollRect SMALL_RECT, clipRect SMALL_RECT, destOrigin COORD, char CHAR_INFO) error { |
||||||
|
r1, r2, err := scrollConsoleScreenBufferProc.Call(handle, uintptr(unsafe.Pointer(&scrollRect)), uintptr(unsafe.Pointer(&clipRect)), coordToPointer(destOrigin), uintptr(unsafe.Pointer(&char))) |
||||||
|
use(scrollRect) |
||||||
|
use(clipRect) |
||||||
|
use(destOrigin) |
||||||
|
use(char) |
||||||
|
return checkError(r1, r2, err) |
||||||
|
} |
||||||
|
|
||||||
|
// SetConsoleScreenBufferSize sets the size of the console screen buffer.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686044(v=vs.85).aspx.
|
||||||
|
func SetConsoleScreenBufferSize(handle uintptr, coord COORD) error { |
||||||
|
r1, r2, err := setConsoleScreenBufferSizeProc.Call(handle, coordToPointer(coord)) |
||||||
|
use(coord) |
||||||
|
return checkError(r1, r2, err) |
||||||
|
} |
||||||
|
|
||||||
|
// SetConsoleTextAttribute sets the attributes of characters written to the
|
||||||
|
// console screen buffer by the WriteFile or WriteConsole function.
|
||||||
|
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686047(v=vs.85).aspx.
|
||||||
|
func SetConsoleTextAttribute(handle uintptr, attribute uint16) error { |
||||||
|
r1, r2, err := setConsoleTextAttributeProc.Call(handle, uintptr(attribute), 0) |
||||||
|
use(attribute) |
||||||
|
return checkError(r1, r2, err) |
||||||
|
} |
||||||
|
|
||||||
|
// SetConsoleWindowInfo sets the size and position of the console screen buffer's window.
|
||||||
|
// Note that the size and location must be within and no larger than the backing console screen buffer.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686125(v=vs.85).aspx.
|
||||||
|
func SetConsoleWindowInfo(handle uintptr, isAbsolute bool, rect SMALL_RECT) error { |
||||||
|
r1, r2, err := setConsoleWindowInfoProc.Call(handle, uintptr(boolToBOOL(isAbsolute)), uintptr(unsafe.Pointer(&rect))) |
||||||
|
use(isAbsolute) |
||||||
|
use(rect) |
||||||
|
return checkError(r1, r2, err) |
||||||
|
} |
||||||
|
|
||||||
|
// WriteConsoleOutput writes the CHAR_INFOs from the provided buffer to the active console buffer.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687404(v=vs.85).aspx.
|
||||||
|
func WriteConsoleOutput(handle uintptr, buffer []CHAR_INFO, bufferSize COORD, bufferCoord COORD, writeRegion *SMALL_RECT) error { |
||||||
|
r1, r2, err := writeConsoleOutputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), coordToPointer(bufferSize), coordToPointer(bufferCoord), uintptr(unsafe.Pointer(writeRegion))) |
||||||
|
use(buffer) |
||||||
|
use(bufferSize) |
||||||
|
use(bufferCoord) |
||||||
|
return checkError(r1, r2, err) |
||||||
|
} |
||||||
|
|
||||||
|
// ReadConsoleInput reads (and removes) data from the console input buffer.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx.
|
||||||
|
func ReadConsoleInput(handle uintptr, buffer []INPUT_RECORD, count *uint32) error { |
||||||
|
r1, r2, err := readConsoleInputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), uintptr(len(buffer)), uintptr(unsafe.Pointer(count))) |
||||||
|
use(buffer) |
||||||
|
return checkError(r1, r2, err) |
||||||
|
} |
||||||
|
|
||||||
|
// WaitForSingleObject waits for the passed handle to be signaled.
|
||||||
|
// It returns true if the handle was signaled; false otherwise.
|
||||||
|
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx.
|
||||||
|
func WaitForSingleObject(handle uintptr, msWait uint32) (bool, error) { |
||||||
|
r1, _, err := waitForSingleObjectProc.Call(handle, uintptr(uint32(msWait))) |
||||||
|
switch r1 { |
||||||
|
case WAIT_ABANDONED, WAIT_TIMEOUT: |
||||||
|
return false, nil |
||||||
|
case WAIT_SIGNALED: |
||||||
|
return true, nil |
||||||
|
} |
||||||
|
use(msWait) |
||||||
|
return false, err |
||||||
|
} |
||||||
|
|
||||||
|
// String helpers
|
||||||
|
func (info CONSOLE_SCREEN_BUFFER_INFO) String() string { |
||||||
|
return fmt.Sprintf("Size(%v) Cursor(%v) Window(%v) Max(%v)", info.Size, info.CursorPosition, info.Window, info.MaximumWindowSize) |
||||||
|
} |
||||||
|
|
||||||
|
func (coord COORD) String() string { |
||||||
|
return fmt.Sprintf("%v,%v", coord.X, coord.Y) |
||||||
|
} |
||||||
|
|
||||||
|
func (rect SMALL_RECT) String() string { |
||||||
|
return fmt.Sprintf("(%v,%v),(%v,%v)", rect.Left, rect.Top, rect.Right, rect.Bottom) |
||||||
|
} |
||||||
|
|
||||||
|
// checkError evaluates the results of a Windows API call and returns the error if it failed.
|
||||||
|
func checkError(r1, r2 uintptr, err error) error { |
||||||
|
// Windows APIs return non-zero to indicate success
|
||||||
|
if r1 != 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Return the error if provided, otherwise default to EINVAL
|
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return syscall.EINVAL |
||||||
|
} |
||||||
|
|
||||||
|
// coordToPointer converts a COORD into a uintptr (by fooling the type system).
|
||||||
|
func coordToPointer(c COORD) uintptr { |
||||||
|
// Note: This code assumes the two SHORTs are correctly laid out; the "cast" to uint32 is just to get a pointer to pass.
|
||||||
|
return uintptr(*((*uint32)(unsafe.Pointer(&c)))) |
||||||
|
} |
||||||
|
|
||||||
|
// use is a no-op, but the compiler cannot see that it is.
|
||||||
|
// Calling use(p) ensures that p is kept live until that point.
|
||||||
|
func use(p interface{}) {} |
@ -0,0 +1,100 @@ |
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm |
||||||
|
|
||||||
|
import "github.com/Azure/go-ansiterm" |
||||||
|
|
||||||
|
const ( |
||||||
|
FOREGROUND_COLOR_MASK = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE |
||||||
|
BACKGROUND_COLOR_MASK = BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE |
||||||
|
) |
||||||
|
|
||||||
|
// collectAnsiIntoWindowsAttributes modifies the passed Windows text mode flags to reflect the
|
||||||
|
// request represented by the passed ANSI mode.
|
||||||
|
func collectAnsiIntoWindowsAttributes(windowsMode uint16, inverted bool, baseMode uint16, ansiMode int16) (uint16, bool) { |
||||||
|
switch ansiMode { |
||||||
|
|
||||||
|
// Mode styles
|
||||||
|
case ansiterm.ANSI_SGR_BOLD: |
||||||
|
windowsMode = windowsMode | FOREGROUND_INTENSITY |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_DIM, ansiterm.ANSI_SGR_BOLD_DIM_OFF: |
||||||
|
windowsMode &^= FOREGROUND_INTENSITY |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_UNDERLINE: |
||||||
|
windowsMode = windowsMode | COMMON_LVB_UNDERSCORE |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_REVERSE: |
||||||
|
inverted = true |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_REVERSE_OFF: |
||||||
|
inverted = false |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_UNDERLINE_OFF: |
||||||
|
windowsMode &^= COMMON_LVB_UNDERSCORE |
||||||
|
|
||||||
|
// Foreground colors
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_DEFAULT: |
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_MASK) | (baseMode & FOREGROUND_MASK) |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_BLACK: |
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_RED: |
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_GREEN: |
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_GREEN |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_YELLOW: |
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_GREEN |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_BLUE: |
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_BLUE |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_MAGENTA: |
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_BLUE |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_CYAN: |
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_GREEN | FOREGROUND_BLUE |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_FOREGROUND_WHITE: |
||||||
|
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE |
||||||
|
|
||||||
|
// Background colors
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_DEFAULT: |
||||||
|
// Black with no intensity
|
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_MASK) | (baseMode & BACKGROUND_MASK) |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_BLACK: |
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_RED: |
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_GREEN: |
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_GREEN |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_YELLOW: |
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_GREEN |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_BLUE: |
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_BLUE |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_MAGENTA: |
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_BLUE |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_CYAN: |
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_GREEN | BACKGROUND_BLUE |
||||||
|
|
||||||
|
case ansiterm.ANSI_SGR_BACKGROUND_WHITE: |
||||||
|
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE |
||||||
|
} |
||||||
|
|
||||||
|
return windowsMode, inverted |
||||||
|
} |
||||||
|
|
||||||
|
// invertAttributes inverts the foreground and background colors of a Windows attributes value
|
||||||
|
func invertAttributes(windowsMode uint16) uint16 { |
||||||
|
return (COMMON_LVB_MASK & windowsMode) | ((FOREGROUND_MASK & windowsMode) << 4) | ((BACKGROUND_MASK & windowsMode) >> 4) |
||||||
|
} |
@ -0,0 +1,101 @@ |
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm |
||||||
|
|
||||||
|
const ( |
||||||
|
horizontal = iota |
||||||
|
vertical |
||||||
|
) |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) getCursorWindow(info *CONSOLE_SCREEN_BUFFER_INFO) SMALL_RECT { |
||||||
|
if h.originMode { |
||||||
|
sr := h.effectiveSr(info.Window) |
||||||
|
return SMALL_RECT{ |
||||||
|
Top: sr.top, |
||||||
|
Bottom: sr.bottom, |
||||||
|
Left: 0, |
||||||
|
Right: info.Size.X - 1, |
||||||
|
} |
||||||
|
} else { |
||||||
|
return SMALL_RECT{ |
||||||
|
Top: info.Window.Top, |
||||||
|
Bottom: info.Window.Bottom, |
||||||
|
Left: 0, |
||||||
|
Right: info.Size.X - 1, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// setCursorPosition sets the cursor to the specified position, bounded to the screen size
|
||||||
|
func (h *windowsAnsiEventHandler) setCursorPosition(position COORD, window SMALL_RECT) error { |
||||||
|
position.X = ensureInRange(position.X, window.Left, window.Right) |
||||||
|
position.Y = ensureInRange(position.Y, window.Top, window.Bottom) |
||||||
|
err := SetConsoleCursorPosition(h.fd, position) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("Cursor position set: (%d, %d)", position.X, position.Y) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) moveCursorVertical(param int) error { |
||||||
|
return h.moveCursor(vertical, param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) moveCursorHorizontal(param int) error { |
||||||
|
return h.moveCursor(horizontal, param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) moveCursor(moveMode int, param int) error { |
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
position := info.CursorPosition |
||||||
|
switch moveMode { |
||||||
|
case horizontal: |
||||||
|
position.X += int16(param) |
||||||
|
case vertical: |
||||||
|
position.Y += int16(param) |
||||||
|
} |
||||||
|
|
||||||
|
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) moveCursorLine(param int) error { |
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
position := info.CursorPosition |
||||||
|
position.X = 0 |
||||||
|
position.Y += int16(param) |
||||||
|
|
||||||
|
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) moveCursorColumn(param int) error { |
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
position := info.CursorPosition |
||||||
|
position.X = int16(param) - 1 |
||||||
|
|
||||||
|
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,84 @@ |
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm |
||||||
|
|
||||||
|
import "github.com/Azure/go-ansiterm" |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) clearRange(attributes uint16, fromCoord COORD, toCoord COORD) error { |
||||||
|
// Ignore an invalid (negative area) request
|
||||||
|
if toCoord.Y < fromCoord.Y { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
var err error |
||||||
|
|
||||||
|
var coordStart = COORD{} |
||||||
|
var coordEnd = COORD{} |
||||||
|
|
||||||
|
xCurrent, yCurrent := fromCoord.X, fromCoord.Y |
||||||
|
xEnd, yEnd := toCoord.X, toCoord.Y |
||||||
|
|
||||||
|
// Clear any partial initial line
|
||||||
|
if xCurrent > 0 { |
||||||
|
coordStart.X, coordStart.Y = xCurrent, yCurrent |
||||||
|
coordEnd.X, coordEnd.Y = xEnd, yCurrent |
||||||
|
|
||||||
|
err = h.clearRect(attributes, coordStart, coordEnd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
xCurrent = 0 |
||||||
|
yCurrent += 1 |
||||||
|
} |
||||||
|
|
||||||
|
// Clear intervening rectangular section
|
||||||
|
if yCurrent < yEnd { |
||||||
|
coordStart.X, coordStart.Y = xCurrent, yCurrent |
||||||
|
coordEnd.X, coordEnd.Y = xEnd, yEnd-1 |
||||||
|
|
||||||
|
err = h.clearRect(attributes, coordStart, coordEnd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
xCurrent = 0 |
||||||
|
yCurrent = yEnd |
||||||
|
} |
||||||
|
|
||||||
|
// Clear remaining partial ending line
|
||||||
|
coordStart.X, coordStart.Y = xCurrent, yCurrent |
||||||
|
coordEnd.X, coordEnd.Y = xEnd, yEnd |
||||||
|
|
||||||
|
err = h.clearRect(attributes, coordStart, coordEnd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) clearRect(attributes uint16, fromCoord COORD, toCoord COORD) error { |
||||||
|
region := SMALL_RECT{Top: fromCoord.Y, Left: fromCoord.X, Bottom: toCoord.Y, Right: toCoord.X} |
||||||
|
width := toCoord.X - fromCoord.X + 1 |
||||||
|
height := toCoord.Y - fromCoord.Y + 1 |
||||||
|
size := uint32(width) * uint32(height) |
||||||
|
|
||||||
|
if size <= 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
buffer := make([]CHAR_INFO, size) |
||||||
|
|
||||||
|
char := CHAR_INFO{ansiterm.FILL_CHARACTER, attributes} |
||||||
|
for i := 0; i < int(size); i++ { |
||||||
|
buffer[i] = char |
||||||
|
} |
||||||
|
|
||||||
|
err := WriteConsoleOutput(h.fd, buffer, COORD{X: width, Y: height}, COORD{X: 0, Y: 0}, ®ion) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,118 @@ |
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm |
||||||
|
|
||||||
|
// effectiveSr gets the current effective scroll region in buffer coordinates
|
||||||
|
func (h *windowsAnsiEventHandler) effectiveSr(window SMALL_RECT) scrollRegion { |
||||||
|
top := addInRange(window.Top, h.sr.top, window.Top, window.Bottom) |
||||||
|
bottom := addInRange(window.Top, h.sr.bottom, window.Top, window.Bottom) |
||||||
|
if top >= bottom { |
||||||
|
top = window.Top |
||||||
|
bottom = window.Bottom |
||||||
|
} |
||||||
|
return scrollRegion{top: top, bottom: bottom} |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) scrollUp(param int) error { |
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
sr := h.effectiveSr(info.Window) |
||||||
|
return h.scroll(param, sr, info) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) scrollDown(param int) error { |
||||||
|
return h.scrollUp(-param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) deleteLines(param int) error { |
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
start := info.CursorPosition.Y |
||||||
|
sr := h.effectiveSr(info.Window) |
||||||
|
// Lines cannot be inserted or deleted outside the scrolling region.
|
||||||
|
if start >= sr.top && start <= sr.bottom { |
||||||
|
sr.top = start |
||||||
|
return h.scroll(param, sr, info) |
||||||
|
} else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) insertLines(param int) error { |
||||||
|
return h.deleteLines(-param) |
||||||
|
} |
||||||
|
|
||||||
|
// scroll scrolls the provided scroll region by param lines. The scroll region is in buffer coordinates.
|
||||||
|
func (h *windowsAnsiEventHandler) scroll(param int, sr scrollRegion, info *CONSOLE_SCREEN_BUFFER_INFO) error { |
||||||
|
h.logf("scroll: scrollTop: %d, scrollBottom: %d", sr.top, sr.bottom) |
||||||
|
h.logf("scroll: windowTop: %d, windowBottom: %d", info.Window.Top, info.Window.Bottom) |
||||||
|
|
||||||
|
// Copy from and clip to the scroll region (full buffer width)
|
||||||
|
scrollRect := SMALL_RECT{ |
||||||
|
Top: sr.top, |
||||||
|
Bottom: sr.bottom, |
||||||
|
Left: 0, |
||||||
|
Right: info.Size.X - 1, |
||||||
|
} |
||||||
|
|
||||||
|
// Origin to which area should be copied
|
||||||
|
destOrigin := COORD{ |
||||||
|
X: 0, |
||||||
|
Y: sr.top - int16(param), |
||||||
|
} |
||||||
|
|
||||||
|
char := CHAR_INFO{ |
||||||
|
UnicodeChar: ' ', |
||||||
|
Attributes: h.attributes, |
||||||
|
} |
||||||
|
|
||||||
|
if err := ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) deleteCharacters(param int) error { |
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return h.scrollLine(param, info.CursorPosition, info) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) insertCharacters(param int) error { |
||||||
|
return h.deleteCharacters(-param) |
||||||
|
} |
||||||
|
|
||||||
|
// scrollLine scrolls a line horizontally starting at the provided position by a number of columns.
|
||||||
|
func (h *windowsAnsiEventHandler) scrollLine(columns int, position COORD, info *CONSOLE_SCREEN_BUFFER_INFO) error { |
||||||
|
// Copy from and clip to the scroll region (full buffer width)
|
||||||
|
scrollRect := SMALL_RECT{ |
||||||
|
Top: position.Y, |
||||||
|
Bottom: position.Y, |
||||||
|
Left: position.X, |
||||||
|
Right: info.Size.X - 1, |
||||||
|
} |
||||||
|
|
||||||
|
// Origin to which area should be copied
|
||||||
|
destOrigin := COORD{ |
||||||
|
X: position.X - int16(columns), |
||||||
|
Y: position.Y, |
||||||
|
} |
||||||
|
|
||||||
|
char := CHAR_INFO{ |
||||||
|
UnicodeChar: ' ', |
||||||
|
Attributes: h.attributes, |
||||||
|
} |
||||||
|
|
||||||
|
if err := ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm |
||||||
|
|
||||||
|
// AddInRange increments a value by the passed quantity while ensuring the values
|
||||||
|
// always remain within the supplied min / max range.
|
||||||
|
func addInRange(n int16, increment int16, min int16, max int16) int16 { |
||||||
|
return ensureInRange(n+increment, min, max) |
||||||
|
} |
@ -0,0 +1,743 @@ |
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winterm |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"log" |
||||||
|
"os" |
||||||
|
"strconv" |
||||||
|
|
||||||
|
"github.com/Azure/go-ansiterm" |
||||||
|
) |
||||||
|
|
||||||
|
type windowsAnsiEventHandler struct { |
||||||
|
fd uintptr |
||||||
|
file *os.File |
||||||
|
infoReset *CONSOLE_SCREEN_BUFFER_INFO |
||||||
|
sr scrollRegion |
||||||
|
buffer bytes.Buffer |
||||||
|
attributes uint16 |
||||||
|
inverted bool |
||||||
|
wrapNext bool |
||||||
|
drewMarginByte bool |
||||||
|
originMode bool |
||||||
|
marginByte byte |
||||||
|
curInfo *CONSOLE_SCREEN_BUFFER_INFO |
||||||
|
curPos COORD |
||||||
|
logf func(string, ...interface{}) |
||||||
|
} |
||||||
|
|
||||||
|
type Option func(*windowsAnsiEventHandler) |
||||||
|
|
||||||
|
func WithLogf(f func(string, ...interface{})) Option { |
||||||
|
return func(w *windowsAnsiEventHandler) { |
||||||
|
w.logf = f |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func CreateWinEventHandler(fd uintptr, file *os.File, opts ...Option) ansiterm.AnsiEventHandler { |
||||||
|
infoReset, err := GetConsoleScreenBufferInfo(fd) |
||||||
|
if err != nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
h := &windowsAnsiEventHandler{ |
||||||
|
fd: fd, |
||||||
|
file: file, |
||||||
|
infoReset: infoReset, |
||||||
|
attributes: infoReset.Attributes, |
||||||
|
} |
||||||
|
for _, o := range opts { |
||||||
|
o(h) |
||||||
|
} |
||||||
|
|
||||||
|
if isDebugEnv := os.Getenv(ansiterm.LogEnv); isDebugEnv == "1" { |
||||||
|
logFile, _ := os.Create("winEventHandler.log") |
||||||
|
logger := log.New(logFile, "", log.LstdFlags) |
||||||
|
if h.logf != nil { |
||||||
|
l := h.logf |
||||||
|
h.logf = func(s string, v ...interface{}) { |
||||||
|
l(s, v...) |
||||||
|
logger.Printf(s, v...) |
||||||
|
} |
||||||
|
} else { |
||||||
|
h.logf = logger.Printf |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if h.logf == nil { |
||||||
|
h.logf = func(string, ...interface{}) {} |
||||||
|
} |
||||||
|
|
||||||
|
return h |
||||||
|
} |
||||||
|
|
||||||
|
type scrollRegion struct { |
||||||
|
top int16 |
||||||
|
bottom int16 |
||||||
|
} |
||||||
|
|
||||||
|
// simulateLF simulates a LF or CR+LF by scrolling if necessary to handle the
|
||||||
|
// current cursor position and scroll region settings, in which case it returns
|
||||||
|
// true. If no special handling is necessary, then it does nothing and returns
|
||||||
|
// false.
|
||||||
|
//
|
||||||
|
// In the false case, the caller should ensure that a carriage return
|
||||||
|
// and line feed are inserted or that the text is otherwise wrapped.
|
||||||
|
func (h *windowsAnsiEventHandler) simulateLF(includeCR bool) (bool, error) { |
||||||
|
if h.wrapNext { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
h.clearWrap() |
||||||
|
} |
||||||
|
pos, info, err := h.getCurrentInfo() |
||||||
|
if err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
sr := h.effectiveSr(info.Window) |
||||||
|
if pos.Y == sr.bottom { |
||||||
|
// Scrolling is necessary. Let Windows automatically scroll if the scrolling region
|
||||||
|
// is the full window.
|
||||||
|
if sr.top == info.Window.Top && sr.bottom == info.Window.Bottom { |
||||||
|
if includeCR { |
||||||
|
pos.X = 0 |
||||||
|
h.updatePos(pos) |
||||||
|
} |
||||||
|
return false, nil |
||||||
|
} |
||||||
|
|
||||||
|
// A custom scroll region is active. Scroll the window manually to simulate
|
||||||
|
// the LF.
|
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
h.logf("Simulating LF inside scroll region") |
||||||
|
if err := h.scrollUp(1); err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
if includeCR { |
||||||
|
pos.X = 0 |
||||||
|
if err := SetConsoleCursorPosition(h.fd, pos); err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
} |
||||||
|
return true, nil |
||||||
|
|
||||||
|
} else if pos.Y < info.Window.Bottom { |
||||||
|
// Let Windows handle the LF.
|
||||||
|
pos.Y++ |
||||||
|
if includeCR { |
||||||
|
pos.X = 0 |
||||||
|
} |
||||||
|
h.updatePos(pos) |
||||||
|
return false, nil |
||||||
|
} else { |
||||||
|
// The cursor is at the bottom of the screen but outside the scroll
|
||||||
|
// region. Skip the LF.
|
||||||
|
h.logf("Simulating LF outside scroll region") |
||||||
|
if includeCR { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
pos.X = 0 |
||||||
|
if err := SetConsoleCursorPosition(h.fd, pos); err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
} |
||||||
|
return true, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// executeLF executes a LF without a CR.
|
||||||
|
func (h *windowsAnsiEventHandler) executeLF() error { |
||||||
|
handled, err := h.simulateLF(false) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if !handled { |
||||||
|
// Windows LF will reset the cursor column position. Write the LF
|
||||||
|
// and restore the cursor position.
|
||||||
|
pos, _, err := h.getCurrentInfo() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.buffer.WriteByte(ansiterm.ANSI_LINE_FEED) |
||||||
|
if pos.X != 0 { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("Resetting cursor position for LF without CR") |
||||||
|
if err := SetConsoleCursorPosition(h.fd, pos); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) Print(b byte) error { |
||||||
|
if h.wrapNext { |
||||||
|
h.buffer.WriteByte(h.marginByte) |
||||||
|
h.clearWrap() |
||||||
|
if _, err := h.simulateLF(true); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
pos, info, err := h.getCurrentInfo() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if pos.X == info.Size.X-1 { |
||||||
|
h.wrapNext = true |
||||||
|
h.marginByte = b |
||||||
|
} else { |
||||||
|
pos.X++ |
||||||
|
h.updatePos(pos) |
||||||
|
h.buffer.WriteByte(b) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) Execute(b byte) error { |
||||||
|
switch b { |
||||||
|
case ansiterm.ANSI_TAB: |
||||||
|
h.logf("Execute(TAB)") |
||||||
|
// Move to the next tab stop, but preserve auto-wrap if already set.
|
||||||
|
if !h.wrapNext { |
||||||
|
pos, info, err := h.getCurrentInfo() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
pos.X = (pos.X + 8) - pos.X%8 |
||||||
|
if pos.X >= info.Size.X { |
||||||
|
pos.X = info.Size.X - 1 |
||||||
|
} |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := SetConsoleCursorPosition(h.fd, pos); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
|
||||||
|
case ansiterm.ANSI_BEL: |
||||||
|
h.buffer.WriteByte(ansiterm.ANSI_BEL) |
||||||
|
return nil |
||||||
|
|
||||||
|
case ansiterm.ANSI_BACKSPACE: |
||||||
|
if h.wrapNext { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.clearWrap() |
||||||
|
} |
||||||
|
pos, _, err := h.getCurrentInfo() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if pos.X > 0 { |
||||||
|
pos.X-- |
||||||
|
h.updatePos(pos) |
||||||
|
h.buffer.WriteByte(ansiterm.ANSI_BACKSPACE) |
||||||
|
} |
||||||
|
return nil |
||||||
|
|
||||||
|
case ansiterm.ANSI_VERTICAL_TAB, ansiterm.ANSI_FORM_FEED: |
||||||
|
// Treat as true LF.
|
||||||
|
return h.executeLF() |
||||||
|
|
||||||
|
case ansiterm.ANSI_LINE_FEED: |
||||||
|
// Simulate a CR and LF for now since there is no way in go-ansiterm
|
||||||
|
// to tell if the LF should include CR (and more things break when it's
|
||||||
|
// missing than when it's incorrectly added).
|
||||||
|
handled, err := h.simulateLF(true) |
||||||
|
if handled || err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return h.buffer.WriteByte(ansiterm.ANSI_LINE_FEED) |
||||||
|
|
||||||
|
case ansiterm.ANSI_CARRIAGE_RETURN: |
||||||
|
if h.wrapNext { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.clearWrap() |
||||||
|
} |
||||||
|
pos, _, err := h.getCurrentInfo() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if pos.X != 0 { |
||||||
|
pos.X = 0 |
||||||
|
h.updatePos(pos) |
||||||
|
h.buffer.WriteByte(ansiterm.ANSI_CARRIAGE_RETURN) |
||||||
|
} |
||||||
|
return nil |
||||||
|
|
||||||
|
default: |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CUU(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("CUU: [%v]", []string{strconv.Itoa(param)}) |
||||||
|
h.clearWrap() |
||||||
|
return h.moveCursorVertical(-param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CUD(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("CUD: [%v]", []string{strconv.Itoa(param)}) |
||||||
|
h.clearWrap() |
||||||
|
return h.moveCursorVertical(param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CUF(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("CUF: [%v]", []string{strconv.Itoa(param)}) |
||||||
|
h.clearWrap() |
||||||
|
return h.moveCursorHorizontal(param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CUB(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("CUB: [%v]", []string{strconv.Itoa(param)}) |
||||||
|
h.clearWrap() |
||||||
|
return h.moveCursorHorizontal(-param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CNL(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("CNL: [%v]", []string{strconv.Itoa(param)}) |
||||||
|
h.clearWrap() |
||||||
|
return h.moveCursorLine(param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CPL(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("CPL: [%v]", []string{strconv.Itoa(param)}) |
||||||
|
h.clearWrap() |
||||||
|
return h.moveCursorLine(-param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CHA(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("CHA: [%v]", []string{strconv.Itoa(param)}) |
||||||
|
h.clearWrap() |
||||||
|
return h.moveCursorColumn(param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) VPA(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("VPA: [[%d]]", param) |
||||||
|
h.clearWrap() |
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
window := h.getCursorWindow(info) |
||||||
|
position := info.CursorPosition |
||||||
|
position.Y = window.Top + int16(param) - 1 |
||||||
|
return h.setCursorPosition(position, window) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) CUP(row int, col int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("CUP: [[%d %d]]", row, col) |
||||||
|
h.clearWrap() |
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
window := h.getCursorWindow(info) |
||||||
|
position := COORD{window.Left + int16(col) - 1, window.Top + int16(row) - 1} |
||||||
|
return h.setCursorPosition(position, window) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) HVP(row int, col int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("HVP: [[%d %d]]", row, col) |
||||||
|
h.clearWrap() |
||||||
|
return h.CUP(row, col) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) DECTCEM(visible bool) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("DECTCEM: [%v]", []string{strconv.FormatBool(visible)}) |
||||||
|
h.clearWrap() |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) DECOM(enable bool) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("DECOM: [%v]", []string{strconv.FormatBool(enable)}) |
||||||
|
h.clearWrap() |
||||||
|
h.originMode = enable |
||||||
|
return h.CUP(1, 1) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) DECCOLM(use132 bool) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("DECCOLM: [%v]", []string{strconv.FormatBool(use132)}) |
||||||
|
h.clearWrap() |
||||||
|
if err := h.ED(2); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
targetWidth := int16(80) |
||||||
|
if use132 { |
||||||
|
targetWidth = 132 |
||||||
|
} |
||||||
|
if info.Size.X < targetWidth { |
||||||
|
if err := SetConsoleScreenBufferSize(h.fd, COORD{targetWidth, info.Size.Y}); err != nil { |
||||||
|
h.logf("set buffer failed: %v", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
window := info.Window |
||||||
|
window.Left = 0 |
||||||
|
window.Right = targetWidth - 1 |
||||||
|
if err := SetConsoleWindowInfo(h.fd, true, window); err != nil { |
||||||
|
h.logf("set window failed: %v", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
if info.Size.X > targetWidth { |
||||||
|
if err := SetConsoleScreenBufferSize(h.fd, COORD{targetWidth, info.Size.Y}); err != nil { |
||||||
|
h.logf("set buffer failed: %v", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
return SetConsoleCursorPosition(h.fd, COORD{0, 0}) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) ED(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("ED: [%v]", []string{strconv.Itoa(param)}) |
||||||
|
h.clearWrap() |
||||||
|
|
||||||
|
// [J -- Erases from the cursor to the end of the screen, including the cursor position.
|
||||||
|
// [1J -- Erases from the beginning of the screen to the cursor, including the cursor position.
|
||||||
|
// [2J -- Erases the complete display. The cursor does not move.
|
||||||
|
// Notes:
|
||||||
|
// -- Clearing the entire buffer, versus just the Window, works best for Windows Consoles
|
||||||
|
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
var start COORD |
||||||
|
var end COORD |
||||||
|
|
||||||
|
switch param { |
||||||
|
case 0: |
||||||
|
start = info.CursorPosition |
||||||
|
end = COORD{info.Size.X - 1, info.Size.Y - 1} |
||||||
|
|
||||||
|
case 1: |
||||||
|
start = COORD{0, 0} |
||||||
|
end = info.CursorPosition |
||||||
|
|
||||||
|
case 2: |
||||||
|
start = COORD{0, 0} |
||||||
|
end = COORD{info.Size.X - 1, info.Size.Y - 1} |
||||||
|
} |
||||||
|
|
||||||
|
err = h.clearRange(h.attributes, start, end) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// If the whole buffer was cleared, move the window to the top while preserving
|
||||||
|
// the window-relative cursor position.
|
||||||
|
if param == 2 { |
||||||
|
pos := info.CursorPosition |
||||||
|
window := info.Window |
||||||
|
pos.Y -= window.Top |
||||||
|
window.Bottom -= window.Top |
||||||
|
window.Top = 0 |
||||||
|
if err := SetConsoleCursorPosition(h.fd, pos); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := SetConsoleWindowInfo(h.fd, true, window); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) EL(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("EL: [%v]", strconv.Itoa(param)) |
||||||
|
h.clearWrap() |
||||||
|
|
||||||
|
// [K -- Erases from the cursor to the end of the line, including the cursor position.
|
||||||
|
// [1K -- Erases from the beginning of the line to the cursor, including the cursor position.
|
||||||
|
// [2K -- Erases the complete line.
|
||||||
|
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
var start COORD |
||||||
|
var end COORD |
||||||
|
|
||||||
|
switch param { |
||||||
|
case 0: |
||||||
|
start = info.CursorPosition |
||||||
|
end = COORD{info.Size.X, info.CursorPosition.Y} |
||||||
|
|
||||||
|
case 1: |
||||||
|
start = COORD{0, info.CursorPosition.Y} |
||||||
|
end = info.CursorPosition |
||||||
|
|
||||||
|
case 2: |
||||||
|
start = COORD{0, info.CursorPosition.Y} |
||||||
|
end = COORD{info.Size.X, info.CursorPosition.Y} |
||||||
|
} |
||||||
|
|
||||||
|
err = h.clearRange(h.attributes, start, end) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) IL(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("IL: [%v]", strconv.Itoa(param)) |
||||||
|
h.clearWrap() |
||||||
|
return h.insertLines(param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) DL(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("DL: [%v]", strconv.Itoa(param)) |
||||||
|
h.clearWrap() |
||||||
|
return h.deleteLines(param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) ICH(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("ICH: [%v]", strconv.Itoa(param)) |
||||||
|
h.clearWrap() |
||||||
|
return h.insertCharacters(param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) DCH(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("DCH: [%v]", strconv.Itoa(param)) |
||||||
|
h.clearWrap() |
||||||
|
return h.deleteCharacters(param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) SGR(params []int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
strings := []string{} |
||||||
|
for _, v := range params { |
||||||
|
strings = append(strings, strconv.Itoa(v)) |
||||||
|
} |
||||||
|
|
||||||
|
h.logf("SGR: [%v]", strings) |
||||||
|
|
||||||
|
if len(params) <= 0 { |
||||||
|
h.attributes = h.infoReset.Attributes |
||||||
|
h.inverted = false |
||||||
|
} else { |
||||||
|
for _, attr := range params { |
||||||
|
|
||||||
|
if attr == ansiterm.ANSI_SGR_RESET { |
||||||
|
h.attributes = h.infoReset.Attributes |
||||||
|
h.inverted = false |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
h.attributes, h.inverted = collectAnsiIntoWindowsAttributes(h.attributes, h.inverted, h.infoReset.Attributes, int16(attr)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
attributes := h.attributes |
||||||
|
if h.inverted { |
||||||
|
attributes = invertAttributes(attributes) |
||||||
|
} |
||||||
|
err := SetConsoleTextAttribute(h.fd, attributes) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) SU(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("SU: [%v]", []string{strconv.Itoa(param)}) |
||||||
|
h.clearWrap() |
||||||
|
return h.scrollUp(param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) SD(param int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("SD: [%v]", []string{strconv.Itoa(param)}) |
||||||
|
h.clearWrap() |
||||||
|
return h.scrollDown(param) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) DA(params []string) error { |
||||||
|
h.logf("DA: [%v]", params) |
||||||
|
// DA cannot be implemented because it must send data on the VT100 input stream,
|
||||||
|
// which is not available to go-ansiterm.
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) DECSTBM(top int, bottom int) error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("DECSTBM: [%d, %d]", top, bottom) |
||||||
|
|
||||||
|
// Windows is 0 indexed, Linux is 1 indexed
|
||||||
|
h.sr.top = int16(top - 1) |
||||||
|
h.sr.bottom = int16(bottom - 1) |
||||||
|
|
||||||
|
// This command also moves the cursor to the origin.
|
||||||
|
h.clearWrap() |
||||||
|
return h.CUP(1, 1) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) RI() error { |
||||||
|
if err := h.Flush(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.logf("RI: []") |
||||||
|
h.clearWrap() |
||||||
|
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
sr := h.effectiveSr(info.Window) |
||||||
|
if info.CursorPosition.Y == sr.top { |
||||||
|
return h.scrollDown(1) |
||||||
|
} |
||||||
|
|
||||||
|
return h.moveCursorVertical(-1) |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) IND() error { |
||||||
|
h.logf("IND: []") |
||||||
|
return h.executeLF() |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) Flush() error { |
||||||
|
h.curInfo = nil |
||||||
|
if h.buffer.Len() > 0 { |
||||||
|
h.logf("Flush: [%s]", h.buffer.Bytes()) |
||||||
|
if _, err := h.buffer.WriteTo(h.file); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if h.wrapNext && !h.drewMarginByte { |
||||||
|
h.logf("Flush: drawing margin byte '%c'", h.marginByte) |
||||||
|
|
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
charInfo := []CHAR_INFO{{UnicodeChar: uint16(h.marginByte), Attributes: info.Attributes}} |
||||||
|
size := COORD{1, 1} |
||||||
|
position := COORD{0, 0} |
||||||
|
region := SMALL_RECT{Left: info.CursorPosition.X, Top: info.CursorPosition.Y, Right: info.CursorPosition.X, Bottom: info.CursorPosition.Y} |
||||||
|
if err := WriteConsoleOutput(h.fd, charInfo, size, position, ®ion); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
h.drewMarginByte = true |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// cacheConsoleInfo ensures that the current console screen information has been queried
|
||||||
|
// since the last call to Flush(). It must be called before accessing h.curInfo or h.curPos.
|
||||||
|
func (h *windowsAnsiEventHandler) getCurrentInfo() (COORD, *CONSOLE_SCREEN_BUFFER_INFO, error) { |
||||||
|
if h.curInfo == nil { |
||||||
|
info, err := GetConsoleScreenBufferInfo(h.fd) |
||||||
|
if err != nil { |
||||||
|
return COORD{}, nil, err |
||||||
|
} |
||||||
|
h.curInfo = info |
||||||
|
h.curPos = info.CursorPosition |
||||||
|
} |
||||||
|
return h.curPos, h.curInfo, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h *windowsAnsiEventHandler) updatePos(pos COORD) { |
||||||
|
if h.curInfo == nil { |
||||||
|
panic("failed to call getCurrentInfo before calling updatePos") |
||||||
|
} |
||||||
|
h.curPos = pos |
||||||
|
} |
||||||
|
|
||||||
|
// clearWrap clears the state where the cursor is in the margin
|
||||||
|
// waiting for the next character before wrapping the line. This must
|
||||||
|
// be done before most operations that act on the cursor.
|
||||||
|
func (h *windowsAnsiEventHandler) clearWrap() { |
||||||
|
h.wrapNext = false |
||||||
|
h.drewMarginByte = false |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
Copyright (C) 2013 Blake Mizerany |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining |
||||||
|
a copy of this software and associated documentation files (the |
||||||
|
"Software"), to deal in the Software without restriction, including |
||||||
|
without limitation the rights to use, copy, modify, merge, publish, |
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to |
||||||
|
permit persons to whom the Software is furnished to do so, subject to |
||||||
|
the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be |
||||||
|
included in all copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,316 @@ |
|||||||
|
// Package quantile computes approximate quantiles over an unbounded data
|
||||||
|
// stream within low memory and CPU bounds.
|
||||||
|
//
|
||||||
|
// A small amount of accuracy is traded to achieve the above properties.
|
||||||
|
//
|
||||||
|
// Multiple streams can be merged before calling Query to generate a single set
|
||||||
|
// of results. This is meaningful when the streams represent the same type of
|
||||||
|
// data. See Merge and Samples.
|
||||||
|
//
|
||||||
|
// For more detailed information about the algorithm used, see:
|
||||||
|
//
|
||||||
|
// Effective Computation of Biased Quantiles over Data Streams
|
||||||
|
//
|
||||||
|
// http://www.cs.rutgers.edu/~muthu/bquant.pdf
|
||||||
|
package quantile |
||||||
|
|
||||||
|
import ( |
||||||
|
"math" |
||||||
|
"sort" |
||||||
|
) |
||||||
|
|
||||||
|
// Sample holds an observed value and meta information for compression. JSON
|
||||||
|
// tags have been added for convenience.
|
||||||
|
type Sample struct { |
||||||
|
Value float64 `json:",string"` |
||||||
|
Width float64 `json:",string"` |
||||||
|
Delta float64 `json:",string"` |
||||||
|
} |
||||||
|
|
||||||
|
// Samples represents a slice of samples. It implements sort.Interface.
|
||||||
|
type Samples []Sample |
||||||
|
|
||||||
|
func (a Samples) Len() int { return len(a) } |
||||||
|
func (a Samples) Less(i, j int) bool { return a[i].Value < a[j].Value } |
||||||
|
func (a Samples) Swap(i, j int) { a[i], a[j] = a[j], a[i] } |
||||||
|
|
||||||
|
type invariant func(s *stream, r float64) float64 |
||||||
|
|
||||||
|
// NewLowBiased returns an initialized Stream for low-biased quantiles
|
||||||
|
// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but
|
||||||
|
// error guarantees can still be given even for the lower ranks of the data
|
||||||
|
// distribution.
|
||||||
|
//
|
||||||
|
// The provided epsilon is a relative error, i.e. the true quantile of a value
|
||||||
|
// returned by a query is guaranteed to be within (1±Epsilon)*Quantile.
|
||||||
|
//
|
||||||
|
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error
|
||||||
|
// properties.
|
||||||
|
func NewLowBiased(epsilon float64) *Stream { |
||||||
|
ƒ := func(s *stream, r float64) float64 { |
||||||
|
return 2 * epsilon * r |
||||||
|
} |
||||||
|
return newStream(ƒ) |
||||||
|
} |
||||||
|
|
||||||
|
// NewHighBiased returns an initialized Stream for high-biased quantiles
|
||||||
|
// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but
|
||||||
|
// error guarantees can still be given even for the higher ranks of the data
|
||||||
|
// distribution.
|
||||||
|
//
|
||||||
|
// The provided epsilon is a relative error, i.e. the true quantile of a value
|
||||||
|
// returned by a query is guaranteed to be within 1-(1±Epsilon)*(1-Quantile).
|
||||||
|
//
|
||||||
|
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error
|
||||||
|
// properties.
|
||||||
|
func NewHighBiased(epsilon float64) *Stream { |
||||||
|
ƒ := func(s *stream, r float64) float64 { |
||||||
|
return 2 * epsilon * (s.n - r) |
||||||
|
} |
||||||
|
return newStream(ƒ) |
||||||
|
} |
||||||
|
|
||||||
|
// NewTargeted returns an initialized Stream concerned with a particular set of
|
||||||
|
// quantile values that are supplied a priori. Knowing these a priori reduces
|
||||||
|
// space and computation time. The targets map maps the desired quantiles to
|
||||||
|
// their absolute errors, i.e. the true quantile of a value returned by a query
|
||||||
|
// is guaranteed to be within (Quantile±Epsilon).
|
||||||
|
//
|
||||||
|
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties.
|
||||||
|
func NewTargeted(targetMap map[float64]float64) *Stream { |
||||||
|
// Convert map to slice to avoid slow iterations on a map.
|
||||||
|
// ƒ is called on the hot path, so converting the map to a slice
|
||||||
|
// beforehand results in significant CPU savings.
|
||||||
|
targets := targetMapToSlice(targetMap) |
||||||
|
|
||||||
|
ƒ := func(s *stream, r float64) float64 { |
||||||
|
var m = math.MaxFloat64 |
||||||
|
var f float64 |
||||||
|
for _, t := range targets { |
||||||
|
if t.quantile*s.n <= r { |
||||||
|
f = (2 * t.epsilon * r) / t.quantile |
||||||
|
} else { |
||||||
|
f = (2 * t.epsilon * (s.n - r)) / (1 - t.quantile) |
||||||
|
} |
||||||
|
if f < m { |
||||||
|
m = f |
||||||
|
} |
||||||
|
} |
||||||
|
return m |
||||||
|
} |
||||||
|
return newStream(ƒ) |
||||||
|
} |
||||||
|
|
||||||
|
type target struct { |
||||||
|
quantile float64 |
||||||
|
epsilon float64 |
||||||
|
} |
||||||
|
|
||||||
|
func targetMapToSlice(targetMap map[float64]float64) []target { |
||||||
|
targets := make([]target, 0, len(targetMap)) |
||||||
|
|
||||||
|
for quantile, epsilon := range targetMap { |
||||||
|
t := target{ |
||||||
|
quantile: quantile, |
||||||
|
epsilon: epsilon, |
||||||
|
} |
||||||
|
targets = append(targets, t) |
||||||
|
} |
||||||
|
|
||||||
|
return targets |
||||||
|
} |
||||||
|
|
||||||
|
// Stream computes quantiles for a stream of float64s. It is not thread-safe by
|
||||||
|
// design. Take care when using across multiple goroutines.
|
||||||
|
type Stream struct { |
||||||
|
*stream |
||||||
|
b Samples |
||||||
|
sorted bool |
||||||
|
} |
||||||
|
|
||||||
|
func newStream(ƒ invariant) *Stream { |
||||||
|
x := &stream{ƒ: ƒ} |
||||||
|
return &Stream{x, make(Samples, 0, 500), true} |
||||||
|
} |
||||||
|
|
||||||
|
// Insert inserts v into the stream.
|
||||||
|
func (s *Stream) Insert(v float64) { |
||||||
|
s.insert(Sample{Value: v, Width: 1}) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Stream) insert(sample Sample) { |
||||||
|
s.b = append(s.b, sample) |
||||||
|
s.sorted = false |
||||||
|
if len(s.b) == cap(s.b) { |
||||||
|
s.flush() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Query returns the computed qth percentiles value. If s was created with
|
||||||
|
// NewTargeted, and q is not in the set of quantiles provided a priori, Query
|
||||||
|
// will return an unspecified result.
|
||||||
|
func (s *Stream) Query(q float64) float64 { |
||||||
|
if !s.flushed() { |
||||||
|
// Fast path when there hasn't been enough data for a flush;
|
||||||
|
// this also yields better accuracy for small sets of data.
|
||||||
|
l := len(s.b) |
||||||
|
if l == 0 { |
||||||
|
return 0 |
||||||
|
} |
||||||
|
i := int(math.Ceil(float64(l) * q)) |
||||||
|
if i > 0 { |
||||||
|
i -= 1 |
||||||
|
} |
||||||
|
s.maybeSort() |
||||||
|
return s.b[i].Value |
||||||
|
} |
||||||
|
s.flush() |
||||||
|
return s.stream.query(q) |
||||||
|
} |
||||||
|
|
||||||
|
// Merge merges samples into the underlying streams samples. This is handy when
|
||||||
|
// merging multiple streams from separate threads, database shards, etc.
|
||||||
|
//
|
||||||
|
// ATTENTION: This method is broken and does not yield correct results. The
|
||||||
|
// underlying algorithm is not capable of merging streams correctly.
|
||||||
|
func (s *Stream) Merge(samples Samples) { |
||||||
|
sort.Sort(samples) |
||||||
|
s.stream.merge(samples) |
||||||
|
} |
||||||
|
|
||||||
|
// Reset reinitializes and clears the list reusing the samples buffer memory.
|
||||||
|
func (s *Stream) Reset() { |
||||||
|
s.stream.reset() |
||||||
|
s.b = s.b[:0] |
||||||
|
} |
||||||
|
|
||||||
|
// Samples returns stream samples held by s.
|
||||||
|
func (s *Stream) Samples() Samples { |
||||||
|
if !s.flushed() { |
||||||
|
return s.b |
||||||
|
} |
||||||
|
s.flush() |
||||||
|
return s.stream.samples() |
||||||
|
} |
||||||
|
|
||||||
|
// Count returns the total number of samples observed in the stream
|
||||||
|
// since initialization.
|
||||||
|
func (s *Stream) Count() int { |
||||||
|
return len(s.b) + s.stream.count() |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Stream) flush() { |
||||||
|
s.maybeSort() |
||||||
|
s.stream.merge(s.b) |
||||||
|
s.b = s.b[:0] |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Stream) maybeSort() { |
||||||
|
if !s.sorted { |
||||||
|
s.sorted = true |
||||||
|
sort.Sort(s.b) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Stream) flushed() bool { |
||||||
|
return len(s.stream.l) > 0 |
||||||
|
} |
||||||
|
|
||||||
|
type stream struct { |
||||||
|
n float64 |
||||||
|
l []Sample |
||||||
|
ƒ invariant |
||||||
|
} |
||||||
|
|
||||||
|
func (s *stream) reset() { |
||||||
|
s.l = s.l[:0] |
||||||
|
s.n = 0 |
||||||
|
} |
||||||
|
|
||||||
|
func (s *stream) insert(v float64) { |
||||||
|
s.merge(Samples{{v, 1, 0}}) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *stream) merge(samples Samples) { |
||||||
|
// TODO(beorn7): This tries to merge not only individual samples, but
|
||||||
|
// whole summaries. The paper doesn't mention merging summaries at
|
||||||
|
// all. Unittests show that the merging is inaccurate. Find out how to
|
||||||
|
// do merges properly.
|
||||||
|
var r float64 |
||||||
|
i := 0 |
||||||
|
for _, sample := range samples { |
||||||
|
for ; i < len(s.l); i++ { |
||||||
|
c := s.l[i] |
||||||
|
if c.Value > sample.Value { |
||||||
|
// Insert at position i.
|
||||||
|
s.l = append(s.l, Sample{}) |
||||||
|
copy(s.l[i+1:], s.l[i:]) |
||||||
|
s.l[i] = Sample{ |
||||||
|
sample.Value, |
||||||
|
sample.Width, |
||||||
|
math.Max(sample.Delta, math.Floor(s.ƒ(s, r))-1), |
||||||
|
// TODO(beorn7): How to calculate delta correctly?
|
||||||
|
} |
||||||
|
i++ |
||||||
|
goto inserted |
||||||
|
} |
||||||
|
r += c.Width |
||||||
|
} |
||||||
|
s.l = append(s.l, Sample{sample.Value, sample.Width, 0}) |
||||||
|
i++ |
||||||
|
inserted: |
||||||
|
s.n += sample.Width |
||||||
|
r += sample.Width |
||||||
|
} |
||||||
|
s.compress() |
||||||
|
} |
||||||
|
|
||||||
|
func (s *stream) count() int { |
||||||
|
return int(s.n) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *stream) query(q float64) float64 { |
||||||
|
t := math.Ceil(q * s.n) |
||||||
|
t += math.Ceil(s.ƒ(s, t) / 2) |
||||||
|
p := s.l[0] |
||||||
|
var r float64 |
||||||
|
for _, c := range s.l[1:] { |
||||||
|
r += p.Width |
||||||
|
if r+c.Width+c.Delta > t { |
||||||
|
return p.Value |
||||||
|
} |
||||||
|
p = c |
||||||
|
} |
||||||
|
return p.Value |
||||||
|
} |
||||||
|
|
||||||
|
func (s *stream) compress() { |
||||||
|
if len(s.l) < 2 { |
||||||
|
return |
||||||
|
} |
||||||
|
x := s.l[len(s.l)-1] |
||||||
|
xi := len(s.l) - 1 |
||||||
|
r := s.n - 1 - x.Width |
||||||
|
|
||||||
|
for i := len(s.l) - 2; i >= 0; i-- { |
||||||
|
c := s.l[i] |
||||||
|
if c.Width+x.Width+x.Delta <= s.ƒ(s, r) { |
||||||
|
x.Width += c.Width |
||||||
|
s.l[xi] = x |
||||||
|
// Remove element at i.
|
||||||
|
copy(s.l[i:], s.l[i+1:]) |
||||||
|
s.l = s.l[:len(s.l)-1] |
||||||
|
xi -= 1 |
||||||
|
} else { |
||||||
|
x = c |
||||||
|
xi = i |
||||||
|
} |
||||||
|
r -= c.Width |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *stream) samples() Samples { |
||||||
|
samples := make(Samples, len(s.l)) |
||||||
|
copy(samples, s.l) |
||||||
|
return samples |
||||||
|
} |
@ -0,0 +1,558 @@ |
|||||||
|
package command |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"io" |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"runtime" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/docker/cli/cli/config" |
||||||
|
cliconfig "github.com/docker/cli/cli/config" |
||||||
|
"github.com/docker/cli/cli/config/configfile" |
||||||
|
dcontext "github.com/docker/cli/cli/context" |
||||||
|
"github.com/docker/cli/cli/context/docker" |
||||||
|
"github.com/docker/cli/cli/context/store" |
||||||
|
"github.com/docker/cli/cli/debug" |
||||||
|
cliflags "github.com/docker/cli/cli/flags" |
||||||
|
manifeststore "github.com/docker/cli/cli/manifest/store" |
||||||
|
registryclient "github.com/docker/cli/cli/registry/client" |
||||||
|
"github.com/docker/cli/cli/streams" |
||||||
|
"github.com/docker/cli/cli/trust" |
||||||
|
"github.com/docker/cli/cli/version" |
||||||
|
dopts "github.com/docker/cli/opts" |
||||||
|
"github.com/docker/docker/api" |
||||||
|
"github.com/docker/docker/api/types" |
||||||
|
registrytypes "github.com/docker/docker/api/types/registry" |
||||||
|
"github.com/docker/docker/client" |
||||||
|
"github.com/docker/go-connections/tlsconfig" |
||||||
|
"github.com/moby/term" |
||||||
|
"github.com/pkg/errors" |
||||||
|
"github.com/spf13/cobra" |
||||||
|
"github.com/theupdateframework/notary" |
||||||
|
notaryclient "github.com/theupdateframework/notary/client" |
||||||
|
"github.com/theupdateframework/notary/passphrase" |
||||||
|
) |
||||||
|
|
||||||
|
// Streams is an interface which exposes the standard input and output streams
|
||||||
|
type Streams interface { |
||||||
|
In() *streams.In |
||||||
|
Out() *streams.Out |
||||||
|
Err() io.Writer |
||||||
|
} |
||||||
|
|
||||||
|
// Cli represents the docker command line client.
|
||||||
|
type Cli interface { |
||||||
|
Client() client.APIClient |
||||||
|
Out() *streams.Out |
||||||
|
Err() io.Writer |
||||||
|
In() *streams.In |
||||||
|
SetIn(in *streams.In) |
||||||
|
Apply(ops ...DockerCliOption) error |
||||||
|
ConfigFile() *configfile.ConfigFile |
||||||
|
ServerInfo() ServerInfo |
||||||
|
ClientInfo() ClientInfo |
||||||
|
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) |
||||||
|
DefaultVersion() string |
||||||
|
ManifestStore() manifeststore.Store |
||||||
|
RegistryClient(bool) registryclient.RegistryClient |
||||||
|
ContentTrustEnabled() bool |
||||||
|
ContextStore() store.Store |
||||||
|
CurrentContext() string |
||||||
|
StackOrchestrator(flagValue string) (Orchestrator, error) |
||||||
|
DockerEndpoint() docker.Endpoint |
||||||
|
} |
||||||
|
|
||||||
|
// DockerCli is an instance the docker command line client.
|
||||||
|
// Instances of the client can be returned from NewDockerCli.
|
||||||
|
type DockerCli struct { |
||||||
|
configFile *configfile.ConfigFile |
||||||
|
in *streams.In |
||||||
|
out *streams.Out |
||||||
|
err io.Writer |
||||||
|
client client.APIClient |
||||||
|
serverInfo ServerInfo |
||||||
|
clientInfo *ClientInfo |
||||||
|
contentTrust bool |
||||||
|
contextStore store.Store |
||||||
|
currentContext string |
||||||
|
dockerEndpoint docker.Endpoint |
||||||
|
contextStoreConfig store.Config |
||||||
|
} |
||||||
|
|
||||||
|
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
|
||||||
|
func (cli *DockerCli) DefaultVersion() string { |
||||||
|
return cli.ClientInfo().DefaultVersion |
||||||
|
} |
||||||
|
|
||||||
|
// Client returns the APIClient
|
||||||
|
func (cli *DockerCli) Client() client.APIClient { |
||||||
|
return cli.client |
||||||
|
} |
||||||
|
|
||||||
|
// Out returns the writer used for stdout
|
||||||
|
func (cli *DockerCli) Out() *streams.Out { |
||||||
|
return cli.out |
||||||
|
} |
||||||
|
|
||||||
|
// Err returns the writer used for stderr
|
||||||
|
func (cli *DockerCli) Err() io.Writer { |
||||||
|
return cli.err |
||||||
|
} |
||||||
|
|
||||||
|
// SetIn sets the reader used for stdin
|
||||||
|
func (cli *DockerCli) SetIn(in *streams.In) { |
||||||
|
cli.in = in |
||||||
|
} |
||||||
|
|
||||||
|
// In returns the reader used for stdin
|
||||||
|
func (cli *DockerCli) In() *streams.In { |
||||||
|
return cli.in |
||||||
|
} |
||||||
|
|
||||||
|
// ShowHelp shows the command help.
|
||||||
|
func ShowHelp(err io.Writer) func(*cobra.Command, []string) error { |
||||||
|
return func(cmd *cobra.Command, args []string) error { |
||||||
|
cmd.SetOut(err) |
||||||
|
cmd.HelpFunc()(cmd, args) |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ConfigFile returns the ConfigFile
|
||||||
|
func (cli *DockerCli) ConfigFile() *configfile.ConfigFile { |
||||||
|
if cli.configFile == nil { |
||||||
|
cli.loadConfigFile() |
||||||
|
} |
||||||
|
return cli.configFile |
||||||
|
} |
||||||
|
|
||||||
|
func (cli *DockerCli) loadConfigFile() { |
||||||
|
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err) |
||||||
|
} |
||||||
|
|
||||||
|
// ServerInfo returns the server version details for the host this client is
|
||||||
|
// connected to
|
||||||
|
func (cli *DockerCli) ServerInfo() ServerInfo { |
||||||
|
return cli.serverInfo |
||||||
|
} |
||||||
|
|
||||||
|
// ClientInfo returns the client details for the cli
|
||||||
|
func (cli *DockerCli) ClientInfo() ClientInfo { |
||||||
|
if cli.clientInfo == nil { |
||||||
|
if err := cli.loadClientInfo(); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
} |
||||||
|
return *cli.clientInfo |
||||||
|
} |
||||||
|
|
||||||
|
func (cli *DockerCli) loadClientInfo() error { |
||||||
|
var v string |
||||||
|
if cli.client != nil { |
||||||
|
v = cli.client.ClientVersion() |
||||||
|
} else { |
||||||
|
v = api.DefaultVersion |
||||||
|
} |
||||||
|
cli.clientInfo = &ClientInfo{ |
||||||
|
DefaultVersion: v, |
||||||
|
HasExperimental: true, |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ContentTrustEnabled returns whether content trust has been enabled by an
|
||||||
|
// environment variable.
|
||||||
|
func (cli *DockerCli) ContentTrustEnabled() bool { |
||||||
|
return cli.contentTrust |
||||||
|
} |
||||||
|
|
||||||
|
// BuildKitEnabled returns whether buildkit is enabled either through a daemon setting
|
||||||
|
// or otherwise the client-side DOCKER_BUILDKIT environment variable
|
||||||
|
func BuildKitEnabled(si ServerInfo) (bool, error) { |
||||||
|
buildkitEnabled := si.BuildkitVersion == types.BuilderBuildKit |
||||||
|
if buildkitEnv := os.Getenv("DOCKER_BUILDKIT"); buildkitEnv != "" { |
||||||
|
var err error |
||||||
|
buildkitEnabled, err = strconv.ParseBool(buildkitEnv) |
||||||
|
if err != nil { |
||||||
|
return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value") |
||||||
|
} |
||||||
|
} |
||||||
|
return buildkitEnabled, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ManifestStore returns a store for local manifests
|
||||||
|
func (cli *DockerCli) ManifestStore() manifeststore.Store { |
||||||
|
// TODO: support override default location from config file
|
||||||
|
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests")) |
||||||
|
} |
||||||
|
|
||||||
|
// RegistryClient returns a client for communicating with a Docker distribution
|
||||||
|
// registry
|
||||||
|
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient { |
||||||
|
resolver := func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig { |
||||||
|
return ResolveAuthConfig(ctx, cli, index) |
||||||
|
} |
||||||
|
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure) |
||||||
|
} |
||||||
|
|
||||||
|
// InitializeOpt is the type of the functional options passed to DockerCli.Initialize
|
||||||
|
type InitializeOpt func(dockerCli *DockerCli) error |
||||||
|
|
||||||
|
// WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI.
|
||||||
|
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) InitializeOpt { |
||||||
|
return func(dockerCli *DockerCli) error { |
||||||
|
var err error |
||||||
|
dockerCli.client, err = makeClient(dockerCli) |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Initialize the dockerCli runs initialization that must happen after command
|
||||||
|
// line flags are parsed.
|
||||||
|
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...InitializeOpt) error { |
||||||
|
var err error |
||||||
|
|
||||||
|
for _, o := range ops { |
||||||
|
if err := o(cli); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
cliflags.SetLogLevel(opts.Common.LogLevel) |
||||||
|
|
||||||
|
if opts.ConfigDir != "" { |
||||||
|
cliconfig.SetDir(opts.ConfigDir) |
||||||
|
} |
||||||
|
|
||||||
|
if opts.Common.Debug { |
||||||
|
debug.Enable() |
||||||
|
} |
||||||
|
|
||||||
|
cli.loadConfigFile() |
||||||
|
|
||||||
|
baseContextStore := store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig) |
||||||
|
cli.contextStore = &ContextStoreWithDefault{ |
||||||
|
Store: baseContextStore, |
||||||
|
Resolver: func() (*DefaultContext, error) { |
||||||
|
return ResolveDefaultContext(opts.Common, cli.ConfigFile(), cli.contextStoreConfig, cli.Err()) |
||||||
|
}, |
||||||
|
} |
||||||
|
cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, cli.currentContext) |
||||||
|
if err != nil { |
||||||
|
return errors.Wrap(err, "unable to resolve docker endpoint") |
||||||
|
} |
||||||
|
|
||||||
|
if cli.client == nil { |
||||||
|
cli.client, err = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile) |
||||||
|
if tlsconfig.IsErrEncryptedKey(err) { |
||||||
|
passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil) |
||||||
|
newClient := func(password string) (client.APIClient, error) { |
||||||
|
cli.dockerEndpoint.TLSPassword = password |
||||||
|
return newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile) |
||||||
|
} |
||||||
|
cli.client, err = getClientWithPassword(passRetriever, newClient) |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
cli.initializeFromClient() |
||||||
|
|
||||||
|
if err := cli.loadClientInfo(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// NewAPIClientFromFlags creates a new APIClient from command line flags
|
||||||
|
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) { |
||||||
|
storeConfig := DefaultContextStoreConfig() |
||||||
|
store := &ContextStoreWithDefault{ |
||||||
|
Store: store.New(cliconfig.ContextStoreDir(), storeConfig), |
||||||
|
Resolver: func() (*DefaultContext, error) { |
||||||
|
return ResolveDefaultContext(opts, configFile, storeConfig, ioutil.Discard) |
||||||
|
}, |
||||||
|
} |
||||||
|
contextName, err := resolveContextName(opts, configFile, store) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
endpoint, err := resolveDockerEndpoint(store, contextName) |
||||||
|
if err != nil { |
||||||
|
return nil, errors.Wrap(err, "unable to resolve docker endpoint") |
||||||
|
} |
||||||
|
return newAPIClientFromEndpoint(endpoint, configFile) |
||||||
|
} |
||||||
|
|
||||||
|
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) { |
||||||
|
clientOpts, err := ep.ClientOpts() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
customHeaders := make(map[string]string, len(configFile.HTTPHeaders)) |
||||||
|
for k, v := range configFile.HTTPHeaders { |
||||||
|
customHeaders[k] = v |
||||||
|
} |
||||||
|
customHeaders["User-Agent"] = UserAgent() |
||||||
|
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders)) |
||||||
|
return client.NewClientWithOpts(clientOpts...) |
||||||
|
} |
||||||
|
|
||||||
|
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) { |
||||||
|
ctxMeta, err := s.GetMetadata(contextName) |
||||||
|
if err != nil { |
||||||
|
return docker.Endpoint{}, err |
||||||
|
} |
||||||
|
epMeta, err := docker.EndpointFromContext(ctxMeta) |
||||||
|
if err != nil { |
||||||
|
return docker.Endpoint{}, err |
||||||
|
} |
||||||
|
return docker.WithTLSData(s, contextName, epMeta) |
||||||
|
} |
||||||
|
|
||||||
|
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
|
||||||
|
func resolveDefaultDockerEndpoint(opts *cliflags.CommonOptions) (docker.Endpoint, error) { |
||||||
|
host, err := getServerHost(opts.Hosts, opts.TLSOptions) |
||||||
|
if err != nil { |
||||||
|
return docker.Endpoint{}, err |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
skipTLSVerify bool |
||||||
|
tlsData *dcontext.TLSData |
||||||
|
) |
||||||
|
|
||||||
|
if opts.TLSOptions != nil { |
||||||
|
skipTLSVerify = opts.TLSOptions.InsecureSkipVerify |
||||||
|
tlsData, err = dcontext.TLSDataFromFiles(opts.TLSOptions.CAFile, opts.TLSOptions.CertFile, opts.TLSOptions.KeyFile) |
||||||
|
if err != nil { |
||||||
|
return docker.Endpoint{}, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return docker.Endpoint{ |
||||||
|
EndpointMeta: docker.EndpointMeta{ |
||||||
|
Host: host, |
||||||
|
SkipTLSVerify: skipTLSVerify, |
||||||
|
}, |
||||||
|
TLSData: tlsData, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (cli *DockerCli) initializeFromClient() { |
||||||
|
ctx := context.Background() |
||||||
|
if strings.HasPrefix(cli.DockerEndpoint().Host, "tcp://") { |
||||||
|
// @FIXME context.WithTimeout doesn't work with connhelper / ssh connections
|
||||||
|
// time="2020-04-10T10:16:26Z" level=warning msg="commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
|
||||||
|
var cancel func() |
||||||
|
ctx, cancel = context.WithTimeout(ctx, 2*time.Second) |
||||||
|
defer cancel() |
||||||
|
} |
||||||
|
|
||||||
|
ping, err := cli.client.Ping(ctx) |
||||||
|
if err != nil { |
||||||
|
// Default to true if we fail to connect to daemon
|
||||||
|
cli.serverInfo = ServerInfo{HasExperimental: true} |
||||||
|
|
||||||
|
if ping.APIVersion != "" { |
||||||
|
cli.client.NegotiateAPIVersionPing(ping) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
cli.serverInfo = ServerInfo{ |
||||||
|
HasExperimental: ping.Experimental, |
||||||
|
OSType: ping.OSType, |
||||||
|
BuildkitVersion: ping.BuilderVersion, |
||||||
|
} |
||||||
|
cli.client.NegotiateAPIVersionPing(ping) |
||||||
|
} |
||||||
|
|
||||||
|
func getClientWithPassword(passRetriever notary.PassRetriever, newClient func(password string) (client.APIClient, error)) (client.APIClient, error) { |
||||||
|
for attempts := 0; ; attempts++ { |
||||||
|
passwd, giveup, err := passRetriever("private", "encrypted TLS private", false, attempts) |
||||||
|
if giveup || err != nil { |
||||||
|
return nil, errors.Wrap(err, "private key is encrypted, but could not get passphrase") |
||||||
|
} |
||||||
|
|
||||||
|
apiclient, err := newClient(passwd) |
||||||
|
if !tlsconfig.IsErrEncryptedKey(err) { |
||||||
|
return apiclient, err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
|
||||||
|
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) { |
||||||
|
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...) |
||||||
|
} |
||||||
|
|
||||||
|
// ContextStore returns the ContextStore
|
||||||
|
func (cli *DockerCli) ContextStore() store.Store { |
||||||
|
return cli.contextStore |
||||||
|
} |
||||||
|
|
||||||
|
// CurrentContext returns the current context name
|
||||||
|
func (cli *DockerCli) CurrentContext() string { |
||||||
|
return cli.currentContext |
||||||
|
} |
||||||
|
|
||||||
|
// StackOrchestrator resolves which stack orchestrator is in use
|
||||||
|
func (cli *DockerCli) StackOrchestrator(flagValue string) (Orchestrator, error) { |
||||||
|
currentContext := cli.CurrentContext() |
||||||
|
ctxRaw, err := cli.ContextStore().GetMetadata(currentContext) |
||||||
|
if store.IsErrContextDoesNotExist(err) { |
||||||
|
// case where the currentContext has been removed (CLI behavior is to fallback to using DOCKER_HOST based resolution)
|
||||||
|
return GetStackOrchestrator(flagValue, "", cli.ConfigFile().StackOrchestrator, cli.Err()) |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
ctxMeta, err := GetDockerContext(ctxRaw) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
ctxOrchestrator := string(ctxMeta.StackOrchestrator) |
||||||
|
return GetStackOrchestrator(flagValue, ctxOrchestrator, cli.ConfigFile().StackOrchestrator, cli.Err()) |
||||||
|
} |
||||||
|
|
||||||
|
// DockerEndpoint returns the current docker endpoint
|
||||||
|
func (cli *DockerCli) DockerEndpoint() docker.Endpoint { |
||||||
|
return cli.dockerEndpoint |
||||||
|
} |
||||||
|
|
||||||
|
// Apply all the operation on the cli
|
||||||
|
func (cli *DockerCli) Apply(ops ...DockerCliOption) error { |
||||||
|
for _, op := range ops { |
||||||
|
if err := op(cli); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ServerInfo stores details about the supported features and platform of the
|
||||||
|
// server
|
||||||
|
type ServerInfo struct { |
||||||
|
HasExperimental bool |
||||||
|
OSType string |
||||||
|
BuildkitVersion types.BuilderVersion |
||||||
|
} |
||||||
|
|
||||||
|
// ClientInfo stores details about the supported features of the client
|
||||||
|
type ClientInfo struct { |
||||||
|
// Deprecated: experimental CLI features always enabled. This field is kept
|
||||||
|
// for backward-compatibility, and is always "true".
|
||||||
|
HasExperimental bool |
||||||
|
DefaultVersion string |
||||||
|
} |
||||||
|
|
||||||
|
// NewDockerCli returns a DockerCli instance with all operators applied on it.
|
||||||
|
// It applies by default the standard streams, and the content trust from
|
||||||
|
// environment.
|
||||||
|
func NewDockerCli(ops ...DockerCliOption) (*DockerCli, error) { |
||||||
|
cli := &DockerCli{} |
||||||
|
defaultOps := []DockerCliOption{ |
||||||
|
WithContentTrustFromEnv(), |
||||||
|
} |
||||||
|
cli.contextStoreConfig = DefaultContextStoreConfig() |
||||||
|
ops = append(defaultOps, ops...) |
||||||
|
if err := cli.Apply(ops...); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if cli.out == nil || cli.in == nil || cli.err == nil { |
||||||
|
stdin, stdout, stderr := term.StdStreams() |
||||||
|
if cli.in == nil { |
||||||
|
cli.in = streams.NewIn(stdin) |
||||||
|
} |
||||||
|
if cli.out == nil { |
||||||
|
cli.out = streams.NewOut(stdout) |
||||||
|
} |
||||||
|
if cli.err == nil { |
||||||
|
cli.err = stderr |
||||||
|
} |
||||||
|
} |
||||||
|
return cli, nil |
||||||
|
} |
||||||
|
|
||||||
|
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) { |
||||||
|
var host string |
||||||
|
switch len(hosts) { |
||||||
|
case 0: |
||||||
|
host = os.Getenv("DOCKER_HOST") |
||||||
|
case 1: |
||||||
|
host = hosts[0] |
||||||
|
default: |
||||||
|
return "", errors.New("Please specify only one -H") |
||||||
|
} |
||||||
|
|
||||||
|
return dopts.ParseHost(tlsOptions != nil, host) |
||||||
|
} |
||||||
|
|
||||||
|
// UserAgent returns the user agent string used for making API requests
|
||||||
|
func UserAgent() string { |
||||||
|
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")" |
||||||
|
} |
||||||
|
|
||||||
|
// resolveContextName resolves the current context name with the following rules:
|
||||||
|
// - setting both --context and --host flags is ambiguous
|
||||||
|
// - if --context is set, use this value
|
||||||
|
// - if --host flag or DOCKER_HOST is set, fallbacks to use the same logic as before context-store was added
|
||||||
|
// for backward compatibility with existing scripts
|
||||||
|
// - if DOCKER_CONTEXT is set, use this value
|
||||||
|
// - if Config file has a globally set "CurrentContext", use this value
|
||||||
|
// - fallbacks to default HOST, uses TLS config from flags/env vars
|
||||||
|
func resolveContextName(opts *cliflags.CommonOptions, config *configfile.ConfigFile, contextstore store.Reader) (string, error) { |
||||||
|
if opts.Context != "" && len(opts.Hosts) > 0 { |
||||||
|
return "", errors.New("Conflicting options: either specify --host or --context, not both") |
||||||
|
} |
||||||
|
if opts.Context != "" { |
||||||
|
return opts.Context, nil |
||||||
|
} |
||||||
|
if len(opts.Hosts) > 0 { |
||||||
|
return DefaultContextName, nil |
||||||
|
} |
||||||
|
if _, present := os.LookupEnv("DOCKER_HOST"); present { |
||||||
|
return DefaultContextName, nil |
||||||
|
} |
||||||
|
if ctxName, ok := os.LookupEnv("DOCKER_CONTEXT"); ok { |
||||||
|
return ctxName, nil |
||||||
|
} |
||||||
|
if config != nil && config.CurrentContext != "" { |
||||||
|
_, err := contextstore.GetMetadata(config.CurrentContext) |
||||||
|
if store.IsErrContextDoesNotExist(err) { |
||||||
|
return "", errors.Errorf("Current context %q is not found on the file system, please check your config file at %s", config.CurrentContext, config.Filename) |
||||||
|
} |
||||||
|
return config.CurrentContext, err |
||||||
|
} |
||||||
|
return DefaultContextName, nil |
||||||
|
} |
||||||
|
|
||||||
|
var defaultStoreEndpoints = []store.NamedTypeGetter{ |
||||||
|
store.EndpointTypeGetter(docker.DockerEndpoint, func() interface{} { return &docker.EndpointMeta{} }), |
||||||
|
} |
||||||
|
|
||||||
|
// RegisterDefaultStoreEndpoints registers a new named endpoint
|
||||||
|
// metadata type with the default context store config, so that
|
||||||
|
// endpoint will be supported by stores using the config returned by
|
||||||
|
// DefaultContextStoreConfig.
|
||||||
|
func RegisterDefaultStoreEndpoints(ep ...store.NamedTypeGetter) { |
||||||
|
defaultStoreEndpoints = append(defaultStoreEndpoints, ep...) |
||||||
|
} |
||||||
|
|
||||||
|
// DefaultContextStoreConfig returns a new store.Config with the default set of endpoints configured.
|
||||||
|
func DefaultContextStoreConfig() store.Config { |
||||||
|
return store.NewConfig( |
||||||
|
func() interface{} { return &DockerContext{} }, |
||||||
|
defaultStoreEndpoints..., |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,96 @@ |
|||||||
|
package command |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"strconv" |
||||||
|
|
||||||
|
"github.com/docker/cli/cli/context/docker" |
||||||
|
"github.com/docker/cli/cli/context/store" |
||||||
|
"github.com/docker/cli/cli/streams" |
||||||
|
"github.com/moby/term" |
||||||
|
) |
||||||
|
|
||||||
|
// DockerCliOption applies a modification on a DockerCli.
|
||||||
|
type DockerCliOption func(cli *DockerCli) error |
||||||
|
|
||||||
|
// WithStandardStreams sets a cli in, out and err streams with the standard streams.
|
||||||
|
func WithStandardStreams() DockerCliOption { |
||||||
|
return func(cli *DockerCli) error { |
||||||
|
// Set terminal emulation based on platform as required.
|
||||||
|
stdin, stdout, stderr := term.StdStreams() |
||||||
|
cli.in = streams.NewIn(stdin) |
||||||
|
cli.out = streams.NewOut(stdout) |
||||||
|
cli.err = stderr |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// WithCombinedStreams uses the same stream for the output and error streams.
|
||||||
|
func WithCombinedStreams(combined io.Writer) DockerCliOption { |
||||||
|
return func(cli *DockerCli) error { |
||||||
|
cli.out = streams.NewOut(combined) |
||||||
|
cli.err = combined |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// WithInputStream sets a cli input stream.
|
||||||
|
func WithInputStream(in io.ReadCloser) DockerCliOption { |
||||||
|
return func(cli *DockerCli) error { |
||||||
|
cli.in = streams.NewIn(in) |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// WithOutputStream sets a cli output stream.
|
||||||
|
func WithOutputStream(out io.Writer) DockerCliOption { |
||||||
|
return func(cli *DockerCli) error { |
||||||
|
cli.out = streams.NewOut(out) |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// WithErrorStream sets a cli error stream.
|
||||||
|
func WithErrorStream(err io.Writer) DockerCliOption { |
||||||
|
return func(cli *DockerCli) error { |
||||||
|
cli.err = err |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
|
||||||
|
func WithContentTrustFromEnv() DockerCliOption { |
||||||
|
return func(cli *DockerCli) error { |
||||||
|
cli.contentTrust = false |
||||||
|
if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { |
||||||
|
if t, err := strconv.ParseBool(e); t || err != nil { |
||||||
|
// treat any other value as true
|
||||||
|
cli.contentTrust = true |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// WithContentTrust enables content trust on a cli.
|
||||||
|
func WithContentTrust(enabled bool) DockerCliOption { |
||||||
|
return func(cli *DockerCli) error { |
||||||
|
cli.contentTrust = enabled |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// WithContextEndpointType add support for an additional typed endpoint in the context store
|
||||||
|
// Plugins should use this to store additional endpoints configuration in the context store
|
||||||
|
func WithContextEndpointType(endpointName string, endpointType store.TypeGetter) DockerCliOption { |
||||||
|
return func(cli *DockerCli) error { |
||||||
|
switch endpointName { |
||||||
|
case docker.DockerEndpoint: |
||||||
|
return fmt.Errorf("cannot change %q endpoint type", endpointName) |
||||||
|
} |
||||||
|
cli.contextStoreConfig.SetEndpoint(endpointName, endpointType) |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,68 @@ |
|||||||
|
package command |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
|
||||||
|
"github.com/docker/cli/cli/context/store" |
||||||
|
) |
||||||
|
|
||||||
|
// DockerContext is a typed representation of what we put in Context metadata
|
||||||
|
type DockerContext struct { |
||||||
|
Description string |
||||||
|
StackOrchestrator Orchestrator |
||||||
|
AdditionalFields map[string]interface{} |
||||||
|
} |
||||||
|
|
||||||
|
// MarshalJSON implements custom JSON marshalling
|
||||||
|
func (dc DockerContext) MarshalJSON() ([]byte, error) { |
||||||
|
s := map[string]interface{}{} |
||||||
|
if dc.Description != "" { |
||||||
|
s["Description"] = dc.Description |
||||||
|
} |
||||||
|
if dc.StackOrchestrator != "" { |
||||||
|
s["StackOrchestrator"] = dc.StackOrchestrator |
||||||
|
} |
||||||
|
if dc.AdditionalFields != nil { |
||||||
|
for k, v := range dc.AdditionalFields { |
||||||
|
s[k] = v |
||||||
|
} |
||||||
|
} |
||||||
|
return json.Marshal(s) |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalJSON implements custom JSON marshalling
|
||||||
|
func (dc *DockerContext) UnmarshalJSON(payload []byte) error { |
||||||
|
var data map[string]interface{} |
||||||
|
if err := json.Unmarshal(payload, &data); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
for k, v := range data { |
||||||
|
switch k { |
||||||
|
case "Description": |
||||||
|
dc.Description = v.(string) |
||||||
|
case "StackOrchestrator": |
||||||
|
dc.StackOrchestrator = Orchestrator(v.(string)) |
||||||
|
default: |
||||||
|
if dc.AdditionalFields == nil { |
||||||
|
dc.AdditionalFields = make(map[string]interface{}) |
||||||
|
} |
||||||
|
dc.AdditionalFields[k] = v |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetDockerContext extracts metadata from stored context metadata
|
||||||
|
func GetDockerContext(storeMetadata store.Metadata) (DockerContext, error) { |
||||||
|
if storeMetadata.Metadata == nil { |
||||||
|
// can happen if we save endpoints before assigning a context metadata
|
||||||
|
// it is totally valid, and we should return a default initialized value
|
||||||
|
return DockerContext{}, nil |
||||||
|
} |
||||||
|
res, ok := storeMetadata.Metadata.(DockerContext) |
||||||
|
if !ok { |
||||||
|
return DockerContext{}, errors.New("context metadata is not a valid DockerContext") |
||||||
|
} |
||||||
|
return res, nil |
||||||
|
} |
@ -0,0 +1,215 @@ |
|||||||
|
package command |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
|
||||||
|
"github.com/docker/cli/cli/config/configfile" |
||||||
|
"github.com/docker/cli/cli/context/docker" |
||||||
|
"github.com/docker/cli/cli/context/store" |
||||||
|
cliflags "github.com/docker/cli/cli/flags" |
||||||
|
"github.com/pkg/errors" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
// DefaultContextName is the name reserved for the default context (config & env based)
|
||||||
|
DefaultContextName = "default" |
||||||
|
) |
||||||
|
|
||||||
|
// DefaultContext contains the default context data for all endpoints
|
||||||
|
type DefaultContext struct { |
||||||
|
Meta store.Metadata |
||||||
|
TLS store.ContextTLSData |
||||||
|
} |
||||||
|
|
||||||
|
// DefaultContextResolver is a function which resolves the default context base on the configuration and the env variables
|
||||||
|
type DefaultContextResolver func() (*DefaultContext, error) |
||||||
|
|
||||||
|
// ContextStoreWithDefault implements the store.Store interface with a support for the default context
|
||||||
|
type ContextStoreWithDefault struct { |
||||||
|
store.Store |
||||||
|
Resolver DefaultContextResolver |
||||||
|
} |
||||||
|
|
||||||
|
// EndpointDefaultResolver is implemented by any EndpointMeta object
|
||||||
|
// which wants to be able to populate the store with whatever their default is.
|
||||||
|
type EndpointDefaultResolver interface { |
||||||
|
// ResolveDefault returns values suitable for storing in store.Metadata.Endpoints
|
||||||
|
// and store.ContextTLSData.Endpoints.
|
||||||
|
//
|
||||||
|
// An error is only returned for something fatal, not simply
|
||||||
|
// the lack of a default (e.g. because the config file which
|
||||||
|
// would contain it is missing). If there is no default then
|
||||||
|
// returns nil, nil, nil.
|
||||||
|
ResolveDefault(Orchestrator) (interface{}, *store.EndpointTLSData, error) |
||||||
|
} |
||||||
|
|
||||||
|
// ResolveDefaultContext creates a Metadata for the current CLI invocation parameters
|
||||||
|
func ResolveDefaultContext(opts *cliflags.CommonOptions, config *configfile.ConfigFile, storeconfig store.Config, stderr io.Writer) (*DefaultContext, error) { |
||||||
|
stackOrchestrator, err := GetStackOrchestrator("", "", config.StackOrchestrator, stderr) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
contextTLSData := store.ContextTLSData{ |
||||||
|
Endpoints: make(map[string]store.EndpointTLSData), |
||||||
|
} |
||||||
|
contextMetadata := store.Metadata{ |
||||||
|
Endpoints: make(map[string]interface{}), |
||||||
|
Metadata: DockerContext{ |
||||||
|
Description: "", |
||||||
|
StackOrchestrator: stackOrchestrator, |
||||||
|
}, |
||||||
|
Name: DefaultContextName, |
||||||
|
} |
||||||
|
|
||||||
|
dockerEP, err := resolveDefaultDockerEndpoint(opts) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
contextMetadata.Endpoints[docker.DockerEndpoint] = dockerEP.EndpointMeta |
||||||
|
if dockerEP.TLSData != nil { |
||||||
|
contextTLSData.Endpoints[docker.DockerEndpoint] = *dockerEP.TLSData.ToStoreTLSData() |
||||||
|
} |
||||||
|
|
||||||
|
if err := storeconfig.ForeachEndpointType(func(n string, get store.TypeGetter) error { |
||||||
|
if n == docker.DockerEndpoint { // handled above
|
||||||
|
return nil |
||||||
|
} |
||||||
|
ep := get() |
||||||
|
if i, ok := ep.(EndpointDefaultResolver); ok { |
||||||
|
meta, tls, err := i.ResolveDefault(stackOrchestrator) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if meta == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
contextMetadata.Endpoints[n] = meta |
||||||
|
if tls != nil { |
||||||
|
contextTLSData.Endpoints[n] = *tls |
||||||
|
} |
||||||
|
} |
||||||
|
// Nothing to be done
|
||||||
|
return nil |
||||||
|
}); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return &DefaultContext{Meta: contextMetadata, TLS: contextTLSData}, nil |
||||||
|
} |
||||||
|
|
||||||
|
// List implements store.Store's List
|
||||||
|
func (s *ContextStoreWithDefault) List() ([]store.Metadata, error) { |
||||||
|
contextList, err := s.Store.List() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defaultContext, err := s.Resolver() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return append(contextList, defaultContext.Meta), nil |
||||||
|
} |
||||||
|
|
||||||
|
// CreateOrUpdate is not allowed for the default context and fails
|
||||||
|
func (s *ContextStoreWithDefault) CreateOrUpdate(meta store.Metadata) error { |
||||||
|
if meta.Name == DefaultContextName { |
||||||
|
return errors.New("default context cannot be created nor updated") |
||||||
|
} |
||||||
|
return s.Store.CreateOrUpdate(meta) |
||||||
|
} |
||||||
|
|
||||||
|
// Remove is not allowed for the default context and fails
|
||||||
|
func (s *ContextStoreWithDefault) Remove(name string) error { |
||||||
|
if name == DefaultContextName { |
||||||
|
return errors.New("default context cannot be removed") |
||||||
|
} |
||||||
|
return s.Store.Remove(name) |
||||||
|
} |
||||||
|
|
||||||
|
// GetMetadata implements store.Store's GetMetadata
|
||||||
|
func (s *ContextStoreWithDefault) GetMetadata(name string) (store.Metadata, error) { |
||||||
|
if name == DefaultContextName { |
||||||
|
defaultContext, err := s.Resolver() |
||||||
|
if err != nil { |
||||||
|
return store.Metadata{}, err |
||||||
|
} |
||||||
|
return defaultContext.Meta, nil |
||||||
|
} |
||||||
|
return s.Store.GetMetadata(name) |
||||||
|
} |
||||||
|
|
||||||
|
// ResetTLSMaterial is not implemented for default context and fails
|
||||||
|
func (s *ContextStoreWithDefault) ResetTLSMaterial(name string, data *store.ContextTLSData) error { |
||||||
|
if name == DefaultContextName { |
||||||
|
return errors.New("The default context store does not support ResetTLSMaterial") |
||||||
|
} |
||||||
|
return s.Store.ResetTLSMaterial(name, data) |
||||||
|
} |
||||||
|
|
||||||
|
// ResetEndpointTLSMaterial is not implemented for default context and fails
|
||||||
|
func (s *ContextStoreWithDefault) ResetEndpointTLSMaterial(contextName string, endpointName string, data *store.EndpointTLSData) error { |
||||||
|
if contextName == DefaultContextName { |
||||||
|
return errors.New("The default context store does not support ResetEndpointTLSMaterial") |
||||||
|
} |
||||||
|
return s.Store.ResetEndpointTLSMaterial(contextName, endpointName, data) |
||||||
|
} |
||||||
|
|
||||||
|
// ListTLSFiles implements store.Store's ListTLSFiles
|
||||||
|
func (s *ContextStoreWithDefault) ListTLSFiles(name string) (map[string]store.EndpointFiles, error) { |
||||||
|
if name == DefaultContextName { |
||||||
|
defaultContext, err := s.Resolver() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
tlsfiles := make(map[string]store.EndpointFiles) |
||||||
|
for epName, epTLSData := range defaultContext.TLS.Endpoints { |
||||||
|
var files store.EndpointFiles |
||||||
|
for filename := range epTLSData.Files { |
||||||
|
files = append(files, filename) |
||||||
|
} |
||||||
|
tlsfiles[epName] = files |
||||||
|
} |
||||||
|
return tlsfiles, nil |
||||||
|
} |
||||||
|
return s.Store.ListTLSFiles(name) |
||||||
|
} |
||||||
|
|
||||||
|
// GetTLSData implements store.Store's GetTLSData
|
||||||
|
func (s *ContextStoreWithDefault) GetTLSData(contextName, endpointName, fileName string) ([]byte, error) { |
||||||
|
if contextName == DefaultContextName { |
||||||
|
defaultContext, err := s.Resolver() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if defaultContext.TLS.Endpoints[endpointName].Files[fileName] == nil { |
||||||
|
return nil, &noDefaultTLSDataError{endpointName: endpointName, fileName: fileName} |
||||||
|
} |
||||||
|
return defaultContext.TLS.Endpoints[endpointName].Files[fileName], nil |
||||||
|
|
||||||
|
} |
||||||
|
return s.Store.GetTLSData(contextName, endpointName, fileName) |
||||||
|
} |
||||||
|
|
||||||
|
type noDefaultTLSDataError struct { |
||||||
|
endpointName string |
||||||
|
fileName string |
||||||
|
} |
||||||
|
|
||||||
|
func (e *noDefaultTLSDataError) Error() string { |
||||||
|
return fmt.Sprintf("tls data for %s/%s/%s does not exist", DefaultContextName, e.endpointName, e.fileName) |
||||||
|
} |
||||||
|
|
||||||
|
// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
|
||||||
|
func (e *noDefaultTLSDataError) NotFound() {} |
||||||
|
|
||||||
|
// IsTLSDataDoesNotExist satisfies github.com/docker/cli/cli/context/store.tlsDataDoesNotExist
|
||||||
|
func (e *noDefaultTLSDataError) IsTLSDataDoesNotExist() {} |
||||||
|
|
||||||
|
// GetStorageInfo implements store.Store's GetStorageInfo
|
||||||
|
func (s *ContextStoreWithDefault) GetStorageInfo(contextName string) store.StorageInfo { |
||||||
|
if contextName == DefaultContextName { |
||||||
|
return store.StorageInfo{MetadataPath: "<IN MEMORY>", TLSPath: "<IN MEMORY>"} |
||||||
|
} |
||||||
|
return s.Store.GetStorageInfo(contextName) |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
package command |
||||||
|
|
||||||
|
import ( |
||||||
|
"sync" |
||||||
|
|
||||||
|
eventtypes "github.com/docker/docker/api/types/events" |
||||||
|
"github.com/sirupsen/logrus" |
||||||
|
) |
||||||
|
|
||||||
|
// EventHandler is abstract interface for user to customize
|
||||||
|
// own handle functions of each type of events
|
||||||
|
type EventHandler interface { |
||||||
|
Handle(action string, h func(eventtypes.Message)) |
||||||
|
Watch(c <-chan eventtypes.Message) |
||||||
|
} |
||||||
|
|
||||||
|
// InitEventHandler initializes and returns an EventHandler
|
||||||
|
func InitEventHandler() EventHandler { |
||||||
|
return &eventHandler{handlers: make(map[string]func(eventtypes.Message))} |
||||||
|
} |
||||||
|
|
||||||
|
type eventHandler struct { |
||||||
|
handlers map[string]func(eventtypes.Message) |
||||||
|
mu sync.Mutex |
||||||
|
} |
||||||
|
|
||||||
|
func (w *eventHandler) Handle(action string, h func(eventtypes.Message)) { |
||||||
|
w.mu.Lock() |
||||||
|
w.handlers[action] = h |
||||||
|
w.mu.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
// Watch ranges over the passed in event chan and processes the events based on the
|
||||||
|
// handlers created for a given action.
|
||||||
|
// To stop watching, close the event chan.
|
||||||
|
func (w *eventHandler) Watch(c <-chan eventtypes.Message) { |
||||||
|
for e := range c { |
||||||
|
w.mu.Lock() |
||||||
|
h, exists := w.handlers[e.Action] |
||||||
|
w.mu.Unlock() |
||||||
|
if !exists { |
||||||
|
continue |
||||||
|
} |
||||||
|
logrus.Debugf("event handler: received event: %v", e) |
||||||
|
go h(e) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,84 @@ |
|||||||
|
package command |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
) |
||||||
|
|
||||||
|
// Orchestrator type acts as an enum describing supported orchestrators.
|
||||||
|
type Orchestrator string |
||||||
|
|
||||||
|
const ( |
||||||
|
// OrchestratorKubernetes orchestrator
|
||||||
|
OrchestratorKubernetes = Orchestrator("kubernetes") |
||||||
|
// OrchestratorSwarm orchestrator
|
||||||
|
OrchestratorSwarm = Orchestrator("swarm") |
||||||
|
// OrchestratorAll orchestrator
|
||||||
|
OrchestratorAll = Orchestrator("all") |
||||||
|
orchestratorUnset = Orchestrator("") |
||||||
|
|
||||||
|
defaultOrchestrator = OrchestratorSwarm |
||||||
|
envVarDockerStackOrchestrator = "DOCKER_STACK_ORCHESTRATOR" |
||||||
|
envVarDockerOrchestrator = "DOCKER_ORCHESTRATOR" |
||||||
|
) |
||||||
|
|
||||||
|
// HasKubernetes returns true if defined orchestrator has Kubernetes capabilities.
|
||||||
|
func (o Orchestrator) HasKubernetes() bool { |
||||||
|
return o == OrchestratorKubernetes || o == OrchestratorAll |
||||||
|
} |
||||||
|
|
||||||
|
// HasSwarm returns true if defined orchestrator has Swarm capabilities.
|
||||||
|
func (o Orchestrator) HasSwarm() bool { |
||||||
|
return o == OrchestratorSwarm || o == OrchestratorAll |
||||||
|
} |
||||||
|
|
||||||
|
// HasAll returns true if defined orchestrator has both Swarm and Kubernetes capabilities.
|
||||||
|
func (o Orchestrator) HasAll() bool { |
||||||
|
return o == OrchestratorAll |
||||||
|
} |
||||||
|
|
||||||
|
func normalize(value string) (Orchestrator, error) { |
||||||
|
switch value { |
||||||
|
case "kubernetes": |
||||||
|
return OrchestratorKubernetes, nil |
||||||
|
case "swarm": |
||||||
|
return OrchestratorSwarm, nil |
||||||
|
case "", "unset": // unset is the old value for orchestratorUnset. Keep accepting this for backward compat
|
||||||
|
return orchestratorUnset, nil |
||||||
|
case "all": |
||||||
|
return OrchestratorAll, nil |
||||||
|
default: |
||||||
|
return defaultOrchestrator, fmt.Errorf("specified orchestrator %q is invalid, please use either kubernetes, swarm or all", value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// NormalizeOrchestrator parses an orchestrator value and checks if it is valid
|
||||||
|
func NormalizeOrchestrator(value string) (Orchestrator, error) { |
||||||
|
return normalize(value) |
||||||
|
} |
||||||
|
|
||||||
|
// GetStackOrchestrator checks DOCKER_STACK_ORCHESTRATOR environment variable and configuration file
|
||||||
|
// orchestrator value and returns user defined Orchestrator.
|
||||||
|
func GetStackOrchestrator(flagValue, contextValue, globalDefault string, stderr io.Writer) (Orchestrator, error) { |
||||||
|
// Check flag
|
||||||
|
if o, err := normalize(flagValue); o != orchestratorUnset { |
||||||
|
return o, err |
||||||
|
} |
||||||
|
// Check environment variable
|
||||||
|
env := os.Getenv(envVarDockerStackOrchestrator) |
||||||
|
if env == "" && os.Getenv(envVarDockerOrchestrator) != "" { |
||||||
|
fmt.Fprintf(stderr, "WARNING: experimental environment variable %s is set. Please use %s instead\n", envVarDockerOrchestrator, envVarDockerStackOrchestrator) |
||||||
|
} |
||||||
|
if o, err := normalize(env); o != orchestratorUnset { |
||||||
|
return o, err |
||||||
|
} |
||||||
|
if o, err := normalize(contextValue); o != orchestratorUnset { |
||||||
|
return o, err |
||||||
|
} |
||||||
|
if o, err := normalize(globalDefault); o != orchestratorUnset { |
||||||
|
return o, err |
||||||
|
} |
||||||
|
// Nothing set, use default orchestrator
|
||||||
|
return defaultOrchestrator, nil |
||||||
|
} |
@ -0,0 +1,214 @@ |
|||||||
|
package command |
||||||
|
|
||||||
|
import ( |
||||||
|
"bufio" |
||||||
|
"context" |
||||||
|
"encoding/base64" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"runtime" |
||||||
|
"strings" |
||||||
|
|
||||||
|
configtypes "github.com/docker/cli/cli/config/types" |
||||||
|
"github.com/docker/cli/cli/debug" |
||||||
|
"github.com/docker/cli/cli/streams" |
||||||
|
"github.com/docker/distribution/reference" |
||||||
|
"github.com/docker/docker/api/types" |
||||||
|
registrytypes "github.com/docker/docker/api/types/registry" |
||||||
|
"github.com/docker/docker/registry" |
||||||
|
"github.com/moby/term" |
||||||
|
"github.com/pkg/errors" |
||||||
|
) |
||||||
|
|
||||||
|
// ElectAuthServer returns the default registry to use (by asking the daemon)
|
||||||
|
func ElectAuthServer(ctx context.Context, cli Cli) string { |
||||||
|
// The daemon `/info` endpoint informs us of the default registry being
|
||||||
|
// used. This is essential in cross-platforms environment, where for
|
||||||
|
// example a Linux client might be interacting with a Windows daemon, hence
|
||||||
|
// the default registry URL might be Windows specific.
|
||||||
|
info, err := cli.Client().Info(ctx) |
||||||
|
if err != nil { |
||||||
|
// Daemon is not responding so use system default.
|
||||||
|
if debug.IsEnabled() { |
||||||
|
// Only report the warning if we're in debug mode to prevent nagging during engine initialization workflows
|
||||||
|
fmt.Fprintf(cli.Err(), "Warning: failed to get default registry endpoint from daemon (%v). Using system default: %s\n", err, registry.IndexServer) |
||||||
|
} |
||||||
|
return registry.IndexServer |
||||||
|
} |
||||||
|
if info.IndexServerAddress == "" { |
||||||
|
if debug.IsEnabled() { |
||||||
|
fmt.Fprintf(cli.Err(), "Warning: Empty registry endpoint from daemon. Using system default: %s\n", registry.IndexServer) |
||||||
|
} |
||||||
|
return registry.IndexServer |
||||||
|
} |
||||||
|
return info.IndexServerAddress |
||||||
|
} |
||||||
|
|
||||||
|
// EncodeAuthToBase64 serializes the auth configuration as JSON base64 payload
|
||||||
|
func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) { |
||||||
|
buf, err := json.Marshal(authConfig) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
return base64.URLEncoding.EncodeToString(buf), nil |
||||||
|
} |
||||||
|
|
||||||
|
// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
|
||||||
|
// for the given command.
|
||||||
|
func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc { |
||||||
|
return func() (string, error) { |
||||||
|
fmt.Fprintf(cli.Out(), "\nPlease login prior to %s:\n", cmdName) |
||||||
|
indexServer := registry.GetAuthConfigKey(index) |
||||||
|
isDefaultRegistry := indexServer == ElectAuthServer(context.Background(), cli) |
||||||
|
authConfig, err := GetDefaultAuthConfig(cli, true, indexServer, isDefaultRegistry) |
||||||
|
if authConfig == nil { |
||||||
|
authConfig = &types.AuthConfig{} |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err) |
||||||
|
} |
||||||
|
err = ConfigureAuth(cli, "", "", authConfig, isDefaultRegistry) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
return EncodeAuthToBase64(*authConfig) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ResolveAuthConfig is like registry.ResolveAuthConfig, but if using the
|
||||||
|
// default index, it uses the default index name for the daemon's platform,
|
||||||
|
// not the client's platform.
|
||||||
|
func ResolveAuthConfig(ctx context.Context, cli Cli, index *registrytypes.IndexInfo) types.AuthConfig { |
||||||
|
configKey := index.Name |
||||||
|
if index.Official { |
||||||
|
configKey = ElectAuthServer(ctx, cli) |
||||||
|
} |
||||||
|
|
||||||
|
a, _ := cli.ConfigFile().GetAuthConfig(configKey) |
||||||
|
return types.AuthConfig(a) |
||||||
|
} |
||||||
|
|
||||||
|
// GetDefaultAuthConfig gets the default auth config given a serverAddress
|
||||||
|
// If credentials for given serverAddress exists in the credential store, the configuration will be populated with values in it
|
||||||
|
func GetDefaultAuthConfig(cli Cli, checkCredStore bool, serverAddress string, isDefaultRegistry bool) (*types.AuthConfig, error) { |
||||||
|
if !isDefaultRegistry { |
||||||
|
serverAddress = registry.ConvertToHostname(serverAddress) |
||||||
|
} |
||||||
|
var authconfig = configtypes.AuthConfig{} |
||||||
|
var err error |
||||||
|
if checkCredStore { |
||||||
|
authconfig, err = cli.ConfigFile().GetAuthConfig(serverAddress) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
authconfig.ServerAddress = serverAddress |
||||||
|
authconfig.IdentityToken = "" |
||||||
|
res := types.AuthConfig(authconfig) |
||||||
|
return &res, nil |
||||||
|
} |
||||||
|
|
||||||
|
// ConfigureAuth handles prompting of user's username and password if needed
|
||||||
|
func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *types.AuthConfig, isDefaultRegistry bool) error { |
||||||
|
// On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210
|
||||||
|
if runtime.GOOS == "windows" { |
||||||
|
cli.SetIn(streams.NewIn(os.Stdin)) |
||||||
|
} |
||||||
|
|
||||||
|
// Some links documenting this:
|
||||||
|
// - https://code.google.com/archive/p/mintty/issues/56
|
||||||
|
// - https://github.com/docker/docker/issues/15272
|
||||||
|
// - https://mintty.github.io/ (compatibility)
|
||||||
|
// Linux will hit this if you attempt `cat | docker login`, and Windows
|
||||||
|
// will hit this if you attempt docker login from mintty where stdin
|
||||||
|
// is a pipe, not a character based console.
|
||||||
|
if flPassword == "" && !cli.In().IsTerminal() { |
||||||
|
return errors.Errorf("Error: Cannot perform an interactive login from a non TTY device") |
||||||
|
} |
||||||
|
|
||||||
|
authconfig.Username = strings.TrimSpace(authconfig.Username) |
||||||
|
|
||||||
|
if flUser = strings.TrimSpace(flUser); flUser == "" { |
||||||
|
if isDefaultRegistry { |
||||||
|
// if this is a default registry (docker hub), then display the following message.
|
||||||
|
fmt.Fprintln(cli.Out(), "Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.") |
||||||
|
} |
||||||
|
promptWithDefault(cli.Out(), "Username", authconfig.Username) |
||||||
|
flUser = readInput(cli.In(), cli.Out()) |
||||||
|
flUser = strings.TrimSpace(flUser) |
||||||
|
if flUser == "" { |
||||||
|
flUser = authconfig.Username |
||||||
|
} |
||||||
|
} |
||||||
|
if flUser == "" { |
||||||
|
return errors.Errorf("Error: Non-null Username Required") |
||||||
|
} |
||||||
|
if flPassword == "" { |
||||||
|
oldState, err := term.SaveState(cli.In().FD()) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
fmt.Fprintf(cli.Out(), "Password: ") |
||||||
|
term.DisableEcho(cli.In().FD(), oldState) |
||||||
|
|
||||||
|
flPassword = readInput(cli.In(), cli.Out()) |
||||||
|
fmt.Fprint(cli.Out(), "\n") |
||||||
|
|
||||||
|
term.RestoreTerminal(cli.In().FD(), oldState) |
||||||
|
if flPassword == "" { |
||||||
|
return errors.Errorf("Error: Password Required") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
authconfig.Username = flUser |
||||||
|
authconfig.Password = flPassword |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func readInput(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) |
||||||
|
} |
||||||
|
|
||||||
|
func promptWithDefault(out io.Writer, prompt string, configDefault string) { |
||||||
|
if configDefault == "" { |
||||||
|
fmt.Fprintf(out, "%s: ", prompt) |
||||||
|
} else { |
||||||
|
fmt.Fprintf(out, "%s (%s): ", prompt, configDefault) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete image
|
||||||
|
func RetrieveAuthTokenFromImage(ctx context.Context, cli Cli, image string) (string, error) { |
||||||
|
// Retrieve encoded auth token from the image reference
|
||||||
|
authConfig, err := resolveAuthConfigFromImage(ctx, cli, image) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
encodedAuth, err := EncodeAuthToBase64(authConfig) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
return encodedAuth, nil |
||||||
|
} |
||||||
|
|
||||||
|
// resolveAuthConfigFromImage retrieves that AuthConfig using the image string
|
||||||
|
func resolveAuthConfigFromImage(ctx context.Context, cli Cli, image string) (types.AuthConfig, error) { |
||||||
|
registryRef, err := reference.ParseNormalizedNamed(image) |
||||||
|
if err != nil { |
||||||
|
return types.AuthConfig{}, err |
||||||
|
} |
||||||
|
repoInfo, err := registry.ParseRepositoryInfo(registryRef) |
||||||
|
if err != nil { |
||||||
|
return types.AuthConfig{}, err |
||||||
|
} |
||||||
|
return ResolveAuthConfig(ctx, cli, repoInfo.Index), nil |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
package command |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/docker/cli/cli/streams" |
||||||
|
) |
||||||
|
|
||||||
|
// InStream is an input stream used by the DockerCli to read user input
|
||||||
|
// Deprecated: Use github.com/docker/cli/cli/streams.In instead
|
||||||
|
type InStream = streams.In |
||||||
|
|
||||||
|
// OutStream is an output stream used by the DockerCli to write normal program
|
||||||
|
// output.
|
||||||
|
// Deprecated: Use github.com/docker/cli/cli/streams.Out instead
|
||||||
|
type OutStream = streams.Out |
||||||
|
|
||||||
|
var ( |
||||||
|
// NewInStream returns a new InStream object from a ReadCloser
|
||||||
|
// Deprecated: Use github.com/docker/cli/cli/streams.NewIn instead
|
||||||
|
NewInStream = streams.NewIn |
||||||
|
// NewOutStream returns a new OutStream object from a Writer
|
||||||
|
// Deprecated: Use github.com/docker/cli/cli/streams.NewOut instead
|
||||||
|
NewOutStream = streams.NewOut |
||||||
|
) |
@ -0,0 +1,15 @@ |
|||||||
|
package command |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/spf13/pflag" |
||||||
|
) |
||||||
|
|
||||||
|
// AddTrustVerificationFlags adds content trust flags to the provided flagset
|
||||||
|
func AddTrustVerificationFlags(fs *pflag.FlagSet, v *bool, trusted bool) { |
||||||
|
fs.BoolVar(v, "disable-content-trust", !trusted, "Skip image verification") |
||||||
|
} |
||||||
|
|
||||||
|
// AddTrustSigningFlags adds "signing" flags to the provided flagset
|
||||||
|
func AddTrustSigningFlags(fs *pflag.FlagSet, v *bool, trusted bool) { |
||||||
|
fs.BoolVar(v, "disable-content-trust", !trusted, "Skip image signing") |
||||||
|
} |
@ -0,0 +1,197 @@ |
|||||||
|
package command |
||||||
|
|
||||||
|
import ( |
||||||
|
"bufio" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"runtime" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/docker/cli/cli/streams" |
||||||
|
"github.com/docker/docker/api/types/filters" |
||||||
|
"github.com/docker/docker/pkg/system" |
||||||
|
"github.com/pkg/errors" |
||||||
|
"github.com/spf13/pflag" |
||||||
|
) |
||||||
|
|
||||||
|
// CopyToFile writes the content of the reader to the specified file
|
||||||
|
func CopyToFile(outfile string, r io.Reader) error { |
||||||
|
// We use sequential file access here to avoid depleting the standby list
|
||||||
|
// on Windows. On Linux, this is a call directly to ioutil.TempFile
|
||||||
|
tmpFile, err := system.TempFileSequential(filepath.Dir(outfile), ".docker_temp_") |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
tmpPath := tmpFile.Name() |
||||||
|
|
||||||
|
_, err = io.Copy(tmpFile, r) |
||||||
|
tmpFile.Close() |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
os.Remove(tmpPath) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if err = os.Rename(tmpPath, outfile); err != nil { |
||||||
|
os.Remove(tmpPath) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// capitalizeFirst capitalizes the first character of string
|
||||||
|
func capitalizeFirst(s string) string { |
||||||
|
switch l := len(s); l { |
||||||
|
case 0: |
||||||
|
return s |
||||||
|
case 1: |
||||||
|
return strings.ToLower(s) |
||||||
|
default: |
||||||
|
return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// PrettyPrint outputs arbitrary data for human formatted output by uppercasing the first letter.
|
||||||
|
func PrettyPrint(i interface{}) string { |
||||||
|
switch t := i.(type) { |
||||||
|
case nil: |
||||||
|
return "None" |
||||||
|
case string: |
||||||
|
return capitalizeFirst(t) |
||||||
|
default: |
||||||
|
return capitalizeFirst(fmt.Sprintf("%s", t)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// PromptForConfirmation requests and checks confirmation from user.
|
||||||
|
// This will display the provided message followed by ' [y/N] '. If
|
||||||
|
// the user input 'y' or 'Y' it returns true other false. If no
|
||||||
|
// message is provided "Are you sure you want to proceed? [y/N] "
|
||||||
|
// will be used instead.
|
||||||
|
func PromptForConfirmation(ins io.Reader, outs io.Writer, message string) bool { |
||||||
|
if message == "" { |
||||||
|
message = "Are you sure you want to proceed?" |
||||||
|
} |
||||||
|
message += " [y/N] " |
||||||
|
|
||||||
|
_, _ = fmt.Fprint(outs, message) |
||||||
|
|
||||||
|
// On Windows, force the use of the regular OS stdin stream.
|
||||||
|
if runtime.GOOS == "windows" { |
||||||
|
ins = streams.NewIn(os.Stdin) |
||||||
|
} |
||||||
|
|
||||||
|
reader := bufio.NewReader(ins) |
||||||
|
answer, _, _ := reader.ReadLine() |
||||||
|
return strings.ToLower(string(answer)) == "y" |
||||||
|
} |
||||||
|
|
||||||
|
// PruneFilters returns consolidated prune filters obtained from config.json and cli
|
||||||
|
func PruneFilters(dockerCli Cli, pruneFilters filters.Args) filters.Args { |
||||||
|
if dockerCli.ConfigFile() == nil { |
||||||
|
return pruneFilters |
||||||
|
} |
||||||
|
for _, f := range dockerCli.ConfigFile().PruneFilters { |
||||||
|
parts := strings.SplitN(f, "=", 2) |
||||||
|
if len(parts) != 2 { |
||||||
|
continue |
||||||
|
} |
||||||
|
if parts[0] == "label" { |
||||||
|
// CLI label filter supersede config.json.
|
||||||
|
// If CLI label filter conflict with config.json,
|
||||||
|
// skip adding label! filter in config.json.
|
||||||
|
if pruneFilters.Contains("label!") && pruneFilters.ExactMatch("label!", parts[1]) { |
||||||
|
continue |
||||||
|
} |
||||||
|
} else if parts[0] == "label!" { |
||||||
|
// CLI label! filter supersede config.json.
|
||||||
|
// If CLI label! filter conflict with config.json,
|
||||||
|
// skip adding label filter in config.json.
|
||||||
|
if pruneFilters.Contains("label") && pruneFilters.ExactMatch("label", parts[1]) { |
||||||
|
continue |
||||||
|
} |
||||||
|
} |
||||||
|
pruneFilters.Add(parts[0], parts[1]) |
||||||
|
} |
||||||
|
|
||||||
|
return pruneFilters |
||||||
|
} |
||||||
|
|
||||||
|
// AddPlatformFlag adds `platform` to a set of flags for API version 1.32 and later.
|
||||||
|
func AddPlatformFlag(flags *pflag.FlagSet, target *string) { |
||||||
|
flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") |
||||||
|
flags.SetAnnotation("platform", "version", []string{"1.32"}) |
||||||
|
} |
||||||
|
|
||||||
|
// ValidateOutputPath validates the output paths of the `export` and `save` commands.
|
||||||
|
func ValidateOutputPath(path string) error { |
||||||
|
dir := filepath.Dir(filepath.Clean(path)) |
||||||
|
if dir != "" && dir != "." { |
||||||
|
if _, err := os.Stat(dir); os.IsNotExist(err) { |
||||||
|
return errors.Errorf("invalid output path: directory %q does not exist", dir) |
||||||
|
} |
||||||
|
} |
||||||
|
// check whether `path` points to a regular file
|
||||||
|
// (if the path exists and doesn't point to a directory)
|
||||||
|
if fileInfo, err := os.Stat(path); !os.IsNotExist(err) { |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if fileInfo.Mode().IsDir() || fileInfo.Mode().IsRegular() { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
if err := ValidateOutputPathFileMode(fileInfo.Mode()); err != nil { |
||||||
|
return errors.Wrapf(err, fmt.Sprintf("invalid output path: %q must be a directory or a regular file", path)) |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ValidateOutputPathFileMode validates the output paths of the `cp` command and serves as a
|
||||||
|
// helper to `ValidateOutputPath`
|
||||||
|
func ValidateOutputPathFileMode(fileMode os.FileMode) error { |
||||||
|
switch { |
||||||
|
case fileMode&os.ModeDevice != 0: |
||||||
|
return errors.New("got a device") |
||||||
|
case fileMode&os.ModeIrregular != 0: |
||||||
|
return errors.New("got an irregular file") |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func stringSliceIndex(s, subs []string) int { |
||||||
|
j := 0 |
||||||
|
if len(subs) > 0 { |
||||||
|
for i, x := range s { |
||||||
|
if j < len(subs) && subs[j] == x { |
||||||
|
j++ |
||||||
|
} else { |
||||||
|
j = 0 |
||||||
|
} |
||||||
|
if len(subs) == j { |
||||||
|
return i + 1 - j |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return -1 |
||||||
|
} |
||||||
|
|
||||||
|
// StringSliceReplaceAt replaces the sub-slice old, with the sub-slice new, in the string
|
||||||
|
// slice s, returning a new slice and a boolean indicating if the replacement happened.
|
||||||
|
// requireIdx is the index at which old needs to be found at (or -1 to disregard that).
|
||||||
|
func StringSliceReplaceAt(s, old, new []string, requireIndex int) ([]string, bool) { |
||||||
|
idx := stringSliceIndex(s, old) |
||||||
|
if (requireIndex != -1 && requireIndex != idx) || idx == -1 { |
||||||
|
return s, false |
||||||
|
} |
||||||
|
out := append([]string{}, s[:idx]...) |
||||||
|
out = append(out, new...) |
||||||
|
out = append(out, s[idx+len(old):]...) |
||||||
|
return out, true |
||||||
|
} |
@ -0,0 +1,163 @@ |
|||||||
|
package config |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/docker/cli/cli/config/configfile" |
||||||
|
"github.com/docker/cli/cli/config/credentials" |
||||||
|
"github.com/docker/cli/cli/config/types" |
||||||
|
"github.com/docker/docker/pkg/homedir" |
||||||
|
"github.com/pkg/errors" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
// ConfigFileName is the name of config file
|
||||||
|
ConfigFileName = "config.json" |
||||||
|
configFileDir = ".docker" |
||||||
|
oldConfigfile = ".dockercfg" |
||||||
|
contextsDir = "contexts" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
initConfigDir = new(sync.Once) |
||||||
|
configDir string |
||||||
|
homeDir string |
||||||
|
) |
||||||
|
|
||||||
|
// resetHomeDir is used in testing to reset the "homeDir" package variable to
|
||||||
|
// force re-lookup of the home directory between tests.
|
||||||
|
func resetHomeDir() { |
||||||
|
homeDir = "" |
||||||
|
} |
||||||
|
|
||||||
|
func getHomeDir() string { |
||||||
|
if homeDir == "" { |
||||||
|
homeDir = homedir.Get() |
||||||
|
} |
||||||
|
return homeDir |
||||||
|
} |
||||||
|
|
||||||
|
// resetConfigDir is used in testing to reset the "configDir" package variable
|
||||||
|
// and its sync.Once to force re-lookup between tests.
|
||||||
|
func resetConfigDir() { |
||||||
|
configDir = "" |
||||||
|
initConfigDir = new(sync.Once) |
||||||
|
} |
||||||
|
|
||||||
|
func setConfigDir() { |
||||||
|
if configDir != "" { |
||||||
|
return |
||||||
|
} |
||||||
|
configDir = os.Getenv("DOCKER_CONFIG") |
||||||
|
if configDir == "" { |
||||||
|
configDir = filepath.Join(getHomeDir(), configFileDir) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Dir returns the directory the configuration file is stored in
|
||||||
|
func Dir() string { |
||||||
|
initConfigDir.Do(setConfigDir) |
||||||
|
return configDir |
||||||
|
} |
||||||
|
|
||||||
|
// ContextStoreDir returns the directory the docker contexts are stored in
|
||||||
|
func ContextStoreDir() string { |
||||||
|
return filepath.Join(Dir(), contextsDir) |
||||||
|
} |
||||||
|
|
||||||
|
// SetDir sets the directory the configuration file is stored in
|
||||||
|
func SetDir(dir string) { |
||||||
|
configDir = filepath.Clean(dir) |
||||||
|
} |
||||||
|
|
||||||
|
// Path returns the path to a file relative to the config dir
|
||||||
|
func Path(p ...string) (string, error) { |
||||||
|
path := filepath.Join(append([]string{Dir()}, p...)...) |
||||||
|
if !strings.HasPrefix(path, Dir()+string(filepath.Separator)) { |
||||||
|
return "", errors.Errorf("path %q is outside of root config directory %q", path, Dir()) |
||||||
|
} |
||||||
|
return path, nil |
||||||
|
} |
||||||
|
|
||||||
|
// LegacyLoadFromReader is a convenience function that creates a ConfigFile object from
|
||||||
|
// a non-nested reader
|
||||||
|
func LegacyLoadFromReader(configData io.Reader) (*configfile.ConfigFile, error) { |
||||||
|
configFile := configfile.ConfigFile{ |
||||||
|
AuthConfigs: make(map[string]types.AuthConfig), |
||||||
|
} |
||||||
|
err := configFile.LegacyLoadFromReader(configData) |
||||||
|
return &configFile, err |
||||||
|
} |
||||||
|
|
||||||
|
// LoadFromReader is a convenience function that creates a ConfigFile object from
|
||||||
|
// a reader
|
||||||
|
func LoadFromReader(configData io.Reader) (*configfile.ConfigFile, error) { |
||||||
|
configFile := configfile.ConfigFile{ |
||||||
|
AuthConfigs: make(map[string]types.AuthConfig), |
||||||
|
} |
||||||
|
err := configFile.LoadFromReader(configData) |
||||||
|
return &configFile, err |
||||||
|
} |
||||||
|
|
||||||
|
// TODO remove this temporary hack, which is used to warn about the deprecated ~/.dockercfg file
|
||||||
|
var printLegacyFileWarning bool |
||||||
|
|
||||||
|
// Load reads the configuration files in the given directory, and sets up
|
||||||
|
// the auth config information and returns values.
|
||||||
|
// FIXME: use the internal golang config parser
|
||||||
|
func Load(configDir string) (*configfile.ConfigFile, error) { |
||||||
|
printLegacyFileWarning = false |
||||||
|
|
||||||
|
if configDir == "" { |
||||||
|
configDir = Dir() |
||||||
|
} |
||||||
|
|
||||||
|
filename := filepath.Join(configDir, ConfigFileName) |
||||||
|
configFile := configfile.New(filename) |
||||||
|
|
||||||
|
// Try happy path first - latest config file
|
||||||
|
if file, err := os.Open(filename); err == nil { |
||||||
|
defer file.Close() |
||||||
|
err = configFile.LoadFromReader(file) |
||||||
|
if err != nil { |
||||||
|
err = errors.Wrap(err, filename) |
||||||
|
} |
||||||
|
return configFile, err |
||||||
|
} else if !os.IsNotExist(err) { |
||||||
|
// if file is there but we can't stat it for any reason other
|
||||||
|
// than it doesn't exist then stop
|
||||||
|
return configFile, errors.Wrap(err, filename) |
||||||
|
} |
||||||
|
|
||||||
|
// Can't find latest config file so check for the old one
|
||||||
|
filename = filepath.Join(getHomeDir(), oldConfigfile) |
||||||
|
if file, err := os.Open(filename); err == nil { |
||||||
|
printLegacyFileWarning = true |
||||||
|
defer file.Close() |
||||||
|
if err := configFile.LegacyLoadFromReader(file); err != nil { |
||||||
|
return configFile, errors.Wrap(err, filename) |
||||||
|
} |
||||||
|
} |
||||||
|
return configFile, nil |
||||||
|
} |
||||||
|
|
||||||
|
// LoadDefaultConfigFile attempts to load the default config file and returns
|
||||||
|
// an initialized ConfigFile struct if none is found.
|
||||||
|
func LoadDefaultConfigFile(stderr io.Writer) *configfile.ConfigFile { |
||||||
|
configFile, err := Load(Dir()) |
||||||
|
if err != nil { |
||||||
|
fmt.Fprintf(stderr, "WARNING: Error loading config file: %v\n", err) |
||||||
|
} |
||||||
|
if printLegacyFileWarning { |
||||||
|
_, _ = fmt.Fprintln(stderr, "WARNING: Support for the legacy ~/.dockercfg configuration file and file-format is deprecated and will be removed in an upcoming release") |
||||||
|
} |
||||||
|
if !configFile.ContainsAuth() { |
||||||
|
configFile.CredentialsStore = credentials.DetectDefaultStore(configFile.CredentialsStore) |
||||||
|
} |
||||||
|
return configFile |
||||||
|
} |
@ -0,0 +1,415 @@ |
|||||||
|
package configfile |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/base64" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/docker/cli/cli/config/credentials" |
||||||
|
"github.com/docker/cli/cli/config/types" |
||||||
|
"github.com/pkg/errors" |
||||||
|
"github.com/sirupsen/logrus" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
// This constant is only used for really old config files when the
|
||||||
|
// URL wasn't saved as part of the config file and it was just
|
||||||
|
// assumed to be this value.
|
||||||
|
defaultIndexServer = "https://index.docker.io/v1/" |
||||||
|
) |
||||||
|
|
||||||
|
// ConfigFile ~/.docker/config.json file info
|
||||||
|
type ConfigFile struct { |
||||||
|
AuthConfigs map[string]types.AuthConfig `json:"auths"` |
||||||
|
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` |
||||||
|
PsFormat string `json:"psFormat,omitempty"` |
||||||
|
ImagesFormat string `json:"imagesFormat,omitempty"` |
||||||
|
NetworksFormat string `json:"networksFormat,omitempty"` |
||||||
|
PluginsFormat string `json:"pluginsFormat,omitempty"` |
||||||
|
VolumesFormat string `json:"volumesFormat,omitempty"` |
||||||
|
StatsFormat string `json:"statsFormat,omitempty"` |
||||||
|
DetachKeys string `json:"detachKeys,omitempty"` |
||||||
|
CredentialsStore string `json:"credsStore,omitempty"` |
||||||
|
CredentialHelpers map[string]string `json:"credHelpers,omitempty"` |
||||||
|
Filename string `json:"-"` // Note: for internal use only
|
||||||
|
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"` |
||||||
|
ServicesFormat string `json:"servicesFormat,omitempty"` |
||||||
|
TasksFormat string `json:"tasksFormat,omitempty"` |
||||||
|
SecretFormat string `json:"secretFormat,omitempty"` |
||||||
|
ConfigFormat string `json:"configFormat,omitempty"` |
||||||
|
NodesFormat string `json:"nodesFormat,omitempty"` |
||||||
|
PruneFilters []string `json:"pruneFilters,omitempty"` |
||||||
|
Proxies map[string]ProxyConfig `json:"proxies,omitempty"` |
||||||
|
Experimental string `json:"experimental,omitempty"` |
||||||
|
StackOrchestrator string `json:"stackOrchestrator,omitempty"` |
||||||
|
Kubernetes *KubernetesConfig `json:"kubernetes,omitempty"` |
||||||
|
CurrentContext string `json:"currentContext,omitempty"` |
||||||
|
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"` |
||||||
|
Plugins map[string]map[string]string `json:"plugins,omitempty"` |
||||||
|
Aliases map[string]string `json:"aliases,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
// ProxyConfig contains proxy configuration settings
|
||||||
|
type ProxyConfig struct { |
||||||
|
HTTPProxy string `json:"httpProxy,omitempty"` |
||||||
|
HTTPSProxy string `json:"httpsProxy,omitempty"` |
||||||
|
NoProxy string `json:"noProxy,omitempty"` |
||||||
|
FTPProxy string `json:"ftpProxy,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
// KubernetesConfig contains Kubernetes orchestrator settings
|
||||||
|
type KubernetesConfig struct { |
||||||
|
AllNamespaces string `json:"allNamespaces,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
// New initializes an empty configuration file for the given filename 'fn'
|
||||||
|
func New(fn string) *ConfigFile { |
||||||
|
return &ConfigFile{ |
||||||
|
AuthConfigs: make(map[string]types.AuthConfig), |
||||||
|
HTTPHeaders: make(map[string]string), |
||||||
|
Filename: fn, |
||||||
|
Plugins: make(map[string]map[string]string), |
||||||
|
Aliases: make(map[string]string), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// LegacyLoadFromReader reads the non-nested configuration data given and sets up the
|
||||||
|
// auth config information with given directory and populates the receiver object
|
||||||
|
func (configFile *ConfigFile) LegacyLoadFromReader(configData io.Reader) error { |
||||||
|
b, err := ioutil.ReadAll(configData) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if err := json.Unmarshal(b, &configFile.AuthConfigs); err != nil { |
||||||
|
arr := strings.Split(string(b), "\n") |
||||||
|
if len(arr) < 2 { |
||||||
|
return errors.Errorf("The Auth config file is empty") |
||||||
|
} |
||||||
|
authConfig := types.AuthConfig{} |
||||||
|
origAuth := strings.Split(arr[0], " = ") |
||||||
|
if len(origAuth) != 2 { |
||||||
|
return errors.Errorf("Invalid Auth config file") |
||||||
|
} |
||||||
|
authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1]) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
authConfig.ServerAddress = defaultIndexServer |
||||||
|
configFile.AuthConfigs[defaultIndexServer] = authConfig |
||||||
|
} else { |
||||||
|
for k, authConfig := range configFile.AuthConfigs { |
||||||
|
authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
authConfig.Auth = "" |
||||||
|
authConfig.ServerAddress = k |
||||||
|
configFile.AuthConfigs[k] = authConfig |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// LoadFromReader reads the configuration data given and sets up the auth config
|
||||||
|
// information with given directory and populates the receiver object
|
||||||
|
func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error { |
||||||
|
if err := json.NewDecoder(configData).Decode(&configFile); err != nil && !errors.Is(err, io.EOF) { |
||||||
|
return err |
||||||
|
} |
||||||
|
var err error |
||||||
|
for addr, ac := range configFile.AuthConfigs { |
||||||
|
if ac.Auth != "" { |
||||||
|
ac.Username, ac.Password, err = decodeAuth(ac.Auth) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
ac.Auth = "" |
||||||
|
ac.ServerAddress = addr |
||||||
|
configFile.AuthConfigs[addr] = ac |
||||||
|
} |
||||||
|
return checkKubernetesConfiguration(configFile.Kubernetes) |
||||||
|
} |
||||||
|
|
||||||
|
// ContainsAuth returns whether there is authentication configured
|
||||||
|
// in this file or not.
|
||||||
|
func (configFile *ConfigFile) ContainsAuth() bool { |
||||||
|
return configFile.CredentialsStore != "" || |
||||||
|
len(configFile.CredentialHelpers) > 0 || |
||||||
|
len(configFile.AuthConfigs) > 0 |
||||||
|
} |
||||||
|
|
||||||
|
// GetAuthConfigs returns the mapping of repo to auth configuration
|
||||||
|
func (configFile *ConfigFile) GetAuthConfigs() map[string]types.AuthConfig { |
||||||
|
return configFile.AuthConfigs |
||||||
|
} |
||||||
|
|
||||||
|
// SaveToWriter encodes and writes out all the authorization information to
|
||||||
|
// the given writer
|
||||||
|
func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error { |
||||||
|
// Encode sensitive data into a new/temp struct
|
||||||
|
tmpAuthConfigs := make(map[string]types.AuthConfig, len(configFile.AuthConfigs)) |
||||||
|
for k, authConfig := range configFile.AuthConfigs { |
||||||
|
authCopy := authConfig |
||||||
|
// encode and save the authstring, while blanking out the original fields
|
||||||
|
authCopy.Auth = encodeAuth(&authCopy) |
||||||
|
authCopy.Username = "" |
||||||
|
authCopy.Password = "" |
||||||
|
authCopy.ServerAddress = "" |
||||||
|
tmpAuthConfigs[k] = authCopy |
||||||
|
} |
||||||
|
|
||||||
|
saveAuthConfigs := configFile.AuthConfigs |
||||||
|
configFile.AuthConfigs = tmpAuthConfigs |
||||||
|
defer func() { configFile.AuthConfigs = saveAuthConfigs }() |
||||||
|
|
||||||
|
// User-Agent header is automatically set, and should not be stored in the configuration
|
||||||
|
for v := range configFile.HTTPHeaders { |
||||||
|
if strings.EqualFold(v, "User-Agent") { |
||||||
|
delete(configFile.HTTPHeaders, v) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
data, err := json.MarshalIndent(configFile, "", "\t") |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
_, err = writer.Write(data) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Save encodes and writes out all the authorization information
|
||||||
|
func (configFile *ConfigFile) Save() (retErr error) { |
||||||
|
if configFile.Filename == "" { |
||||||
|
return errors.Errorf("Can't save config with empty filename") |
||||||
|
} |
||||||
|
|
||||||
|
dir := filepath.Dir(configFile.Filename) |
||||||
|
if err := os.MkdirAll(dir, 0700); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
temp, err := ioutil.TempFile(dir, filepath.Base(configFile.Filename)) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer func() { |
||||||
|
temp.Close() |
||||||
|
if retErr != nil { |
||||||
|
if err := os.Remove(temp.Name()); err != nil { |
||||||
|
logrus.WithError(err).WithField("file", temp.Name()).Debug("Error cleaning up temp file") |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
err = configFile.SaveToWriter(temp) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if err := temp.Close(); err != nil { |
||||||
|
return errors.Wrap(err, "error closing temp file") |
||||||
|
} |
||||||
|
|
||||||
|
// Handle situation where the configfile is a symlink
|
||||||
|
cfgFile := configFile.Filename |
||||||
|
if f, err := os.Readlink(cfgFile); err == nil { |
||||||
|
cfgFile = f |
||||||
|
} |
||||||
|
|
||||||
|
// Try copying the current config file (if any) ownership and permissions
|
||||||
|
copyFilePermissions(cfgFile, temp.Name()) |
||||||
|
return os.Rename(temp.Name(), cfgFile) |
||||||
|
} |
||||||
|
|
||||||
|
// ParseProxyConfig computes proxy configuration by retrieving the config for the provided host and
|
||||||
|
// then checking this against any environment variables provided to the container
|
||||||
|
func (configFile *ConfigFile) ParseProxyConfig(host string, runOpts map[string]*string) map[string]*string { |
||||||
|
var cfgKey string |
||||||
|
|
||||||
|
if _, ok := configFile.Proxies[host]; !ok { |
||||||
|
cfgKey = "default" |
||||||
|
} else { |
||||||
|
cfgKey = host |
||||||
|
} |
||||||
|
|
||||||
|
config := configFile.Proxies[cfgKey] |
||||||
|
permitted := map[string]*string{ |
||||||
|
"HTTP_PROXY": &config.HTTPProxy, |
||||||
|
"HTTPS_PROXY": &config.HTTPSProxy, |
||||||
|
"NO_PROXY": &config.NoProxy, |
||||||
|
"FTP_PROXY": &config.FTPProxy, |
||||||
|
} |
||||||
|
m := runOpts |
||||||
|
if m == nil { |
||||||
|
m = make(map[string]*string) |
||||||
|
} |
||||||
|
for k := range permitted { |
||||||
|
if *permitted[k] == "" { |
||||||
|
continue |
||||||
|
} |
||||||
|
if _, ok := m[k]; !ok { |
||||||
|
m[k] = permitted[k] |
||||||
|
} |
||||||
|
if _, ok := m[strings.ToLower(k)]; !ok { |
||||||
|
m[strings.ToLower(k)] = permitted[k] |
||||||
|
} |
||||||
|
} |
||||||
|
return m |
||||||
|
} |
||||||
|
|
||||||
|
// encodeAuth creates a base64 encoded string to containing authorization information
|
||||||
|
func encodeAuth(authConfig *types.AuthConfig) string { |
||||||
|
if authConfig.Username == "" && authConfig.Password == "" { |
||||||
|
return "" |
||||||
|
} |
||||||
|
|
||||||
|
authStr := authConfig.Username + ":" + authConfig.Password |
||||||
|
msg := []byte(authStr) |
||||||
|
encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg))) |
||||||
|
base64.StdEncoding.Encode(encoded, msg) |
||||||
|
return string(encoded) |
||||||
|
} |
||||||
|
|
||||||
|
// decodeAuth decodes a base64 encoded string and returns username and password
|
||||||
|
func decodeAuth(authStr string) (string, string, error) { |
||||||
|
if authStr == "" { |
||||||
|
return "", "", nil |
||||||
|
} |
||||||
|
|
||||||
|
decLen := base64.StdEncoding.DecodedLen(len(authStr)) |
||||||
|
decoded := make([]byte, decLen) |
||||||
|
authByte := []byte(authStr) |
||||||
|
n, err := base64.StdEncoding.Decode(decoded, authByte) |
||||||
|
if err != nil { |
||||||
|
return "", "", err |
||||||
|
} |
||||||
|
if n > decLen { |
||||||
|
return "", "", errors.Errorf("Something went wrong decoding auth config") |
||||||
|
} |
||||||
|
arr := strings.SplitN(string(decoded), ":", 2) |
||||||
|
if len(arr) != 2 { |
||||||
|
return "", "", errors.Errorf("Invalid auth configuration file") |
||||||
|
} |
||||||
|
password := strings.Trim(arr[1], "\x00") |
||||||
|
return arr[0], password, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetCredentialsStore returns a new credentials store from the settings in the
|
||||||
|
// configuration file
|
||||||
|
func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) credentials.Store { |
||||||
|
if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" { |
||||||
|
return newNativeStore(configFile, helper) |
||||||
|
} |
||||||
|
return credentials.NewFileStore(configFile) |
||||||
|
} |
||||||
|
|
||||||
|
// var for unit testing.
|
||||||
|
var newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store { |
||||||
|
return credentials.NewNativeStore(configFile, helperSuffix) |
||||||
|
} |
||||||
|
|
||||||
|
// GetAuthConfig for a repository from the credential store
|
||||||
|
func (configFile *ConfigFile) GetAuthConfig(registryHostname string) (types.AuthConfig, error) { |
||||||
|
return configFile.GetCredentialsStore(registryHostname).Get(registryHostname) |
||||||
|
} |
||||||
|
|
||||||
|
// getConfiguredCredentialStore returns the credential helper configured for the
|
||||||
|
// given registry, the default credsStore, or the empty string if neither are
|
||||||
|
// configured.
|
||||||
|
func getConfiguredCredentialStore(c *ConfigFile, registryHostname string) string { |
||||||
|
if c.CredentialHelpers != nil && registryHostname != "" { |
||||||
|
if helper, exists := c.CredentialHelpers[registryHostname]; exists { |
||||||
|
return helper |
||||||
|
} |
||||||
|
} |
||||||
|
return c.CredentialsStore |
||||||
|
} |
||||||
|
|
||||||
|
// GetAllCredentials returns all of the credentials stored in all of the
|
||||||
|
// configured credential stores.
|
||||||
|
func (configFile *ConfigFile) GetAllCredentials() (map[string]types.AuthConfig, error) { |
||||||
|
auths := make(map[string]types.AuthConfig) |
||||||
|
addAll := func(from map[string]types.AuthConfig) { |
||||||
|
for reg, ac := range from { |
||||||
|
auths[reg] = ac |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
defaultStore := configFile.GetCredentialsStore("") |
||||||
|
newAuths, err := defaultStore.GetAll() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
addAll(newAuths) |
||||||
|
|
||||||
|
// Auth configs from a registry-specific helper should override those from the default store.
|
||||||
|
for registryHostname := range configFile.CredentialHelpers { |
||||||
|
newAuth, err := configFile.GetAuthConfig(registryHostname) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
auths[registryHostname] = newAuth |
||||||
|
} |
||||||
|
return auths, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetFilename returns the file name that this config file is based on.
|
||||||
|
func (configFile *ConfigFile) GetFilename() string { |
||||||
|
return configFile.Filename |
||||||
|
} |
||||||
|
|
||||||
|
// PluginConfig retrieves the requested option for the given plugin.
|
||||||
|
func (configFile *ConfigFile) PluginConfig(pluginname, option string) (string, bool) { |
||||||
|
if configFile.Plugins == nil { |
||||||
|
return "", false |
||||||
|
} |
||||||
|
pluginConfig, ok := configFile.Plugins[pluginname] |
||||||
|
if !ok { |
||||||
|
return "", false |
||||||
|
} |
||||||
|
value, ok := pluginConfig[option] |
||||||
|
return value, ok |
||||||
|
} |
||||||
|
|
||||||
|
// SetPluginConfig sets the option to the given value for the given
|
||||||
|
// plugin. Passing a value of "" will remove the option. If removing
|
||||||
|
// the final config item for a given plugin then also cleans up the
|
||||||
|
// overall plugin entry.
|
||||||
|
func (configFile *ConfigFile) SetPluginConfig(pluginname, option, value string) { |
||||||
|
if configFile.Plugins == nil { |
||||||
|
configFile.Plugins = make(map[string]map[string]string) |
||||||
|
} |
||||||
|
pluginConfig, ok := configFile.Plugins[pluginname] |
||||||
|
if !ok { |
||||||
|
pluginConfig = make(map[string]string) |
||||||
|
configFile.Plugins[pluginname] = pluginConfig |
||||||
|
} |
||||||
|
if value != "" { |
||||||
|
pluginConfig[option] = value |
||||||
|
} else { |
||||||
|
delete(pluginConfig, option) |
||||||
|
} |
||||||
|
if len(pluginConfig) == 0 { |
||||||
|
delete(configFile.Plugins, pluginname) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func checkKubernetesConfiguration(kubeConfig *KubernetesConfig) error { |
||||||
|
if kubeConfig == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
switch kubeConfig.AllNamespaces { |
||||||
|
case "": |
||||||
|
case "enabled": |
||||||
|
case "disabled": |
||||||
|
default: |
||||||
|
return fmt.Errorf("invalid 'kubernetes.allNamespaces' value, should be 'enabled' or 'disabled': %s", kubeConfig.AllNamespaces) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package configfile |
||||||
|
|
||||||
|
import ( |
||||||
|
"os" |
||||||
|
"syscall" |
||||||
|
) |
||||||
|
|
||||||
|
// copyFilePermissions copies file ownership and permissions from "src" to "dst",
|
||||||
|
// ignoring any error during the process.
|
||||||
|
func copyFilePermissions(src, dst string) { |
||||||
|
var ( |
||||||
|
mode os.FileMode = 0600 |
||||||
|
uid, gid int |
||||||
|
) |
||||||
|
|
||||||
|
fi, err := os.Stat(src) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
if fi.Mode().IsRegular() { |
||||||
|
mode = fi.Mode() |
||||||
|
} |
||||||
|
if err := os.Chmod(dst, mode); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
uid = int(fi.Sys().(*syscall.Stat_t).Uid) |
||||||
|
gid = int(fi.Sys().(*syscall.Stat_t).Gid) |
||||||
|
|
||||||
|
if uid > 0 && gid > 0 { |
||||||
|
_ = os.Chown(dst, uid, gid) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
package configfile |
||||||
|
|
||||||
|
func copyFilePermissions(src, dst string) { |
||||||
|
// TODO implement for Windows
|
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
package credentials |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/docker/cli/cli/config/types" |
||||||
|
) |
||||||
|
|
||||||
|
// Store is the interface that any credentials store must implement.
|
||||||
|
type Store interface { |
||||||
|
// Erase removes credentials from the store for a given server.
|
||||||
|
Erase(serverAddress string) error |
||||||
|
// Get retrieves credentials from the store for a given server.
|
||||||
|
Get(serverAddress string) (types.AuthConfig, error) |
||||||
|
// GetAll retrieves all the credentials from the store.
|
||||||
|
GetAll() (map[string]types.AuthConfig, error) |
||||||
|
// Store saves credentials in the store.
|
||||||
|
Store(authConfig types.AuthConfig) error |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
package credentials |
||||||
|
|
||||||
|
import ( |
||||||
|
exec "golang.org/x/sys/execabs" |
||||||
|
) |
||||||
|
|
||||||
|
// DetectDefaultStore return the default credentials store for the platform if
|
||||||
|
// the store executable is available.
|
||||||
|
func DetectDefaultStore(store string) string { |
||||||
|
platformDefault := defaultCredentialsStore() |
||||||
|
|
||||||
|
// user defined or no default for platform
|
||||||
|
if store != "" || platformDefault == "" { |
||||||
|
return store |
||||||
|
} |
||||||
|
|
||||||
|
if _, err := exec.LookPath(remoteCredentialsPrefix + platformDefault); err == nil { |
||||||
|
return platformDefault |
||||||
|
} |
||||||
|
return "" |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
package credentials |
||||||
|
|
||||||
|
func defaultCredentialsStore() string { |
||||||
|
return "osxkeychain" |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
package credentials |
||||||
|
|
||||||
|
import ( |
||||||
|
"os/exec" |
||||||
|
) |
||||||
|
|
||||||
|
func defaultCredentialsStore() string { |
||||||
|
if _, err := exec.LookPath("pass"); err == nil { |
||||||
|
return "pass" |
||||||
|
} |
||||||
|
|
||||||
|
return "secretservice" |
||||||
|
} |
7
vendor/github.com/docker/cli/cli/config/credentials/default_store_unsupported.go
generated
vendored
7
vendor/github.com/docker/cli/cli/config/credentials/default_store_unsupported.go
generated
vendored
@ -0,0 +1,7 @@ |
|||||||
|
// +build !windows,!darwin,!linux
|
||||||
|
|
||||||
|
package credentials |
||||||
|
|
||||||
|
func defaultCredentialsStore() string { |
||||||
|
return "" |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
package credentials |
||||||
|
|
||||||
|
func defaultCredentialsStore() string { |
||||||
|
return "wincred" |
||||||
|
} |
@ -0,0 +1,81 @@ |
|||||||
|
package credentials |
||||||
|
|
||||||
|
import ( |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/docker/cli/cli/config/types" |
||||||
|
) |
||||||
|
|
||||||
|
type store interface { |
||||||
|
Save() error |
||||||
|
GetAuthConfigs() map[string]types.AuthConfig |
||||||
|
GetFilename() string |
||||||
|
} |
||||||
|
|
||||||
|
// fileStore implements a credentials store using
|
||||||
|
// the docker configuration file to keep the credentials in plain text.
|
||||||
|
type fileStore struct { |
||||||
|
file store |
||||||
|
} |
||||||
|
|
||||||
|
// NewFileStore creates a new file credentials store.
|
||||||
|
func NewFileStore(file store) Store { |
||||||
|
return &fileStore{file: file} |
||||||
|
} |
||||||
|
|
||||||
|
// Erase removes the given credentials from the file store.
|
||||||
|
func (c *fileStore) Erase(serverAddress string) error { |
||||||
|
delete(c.file.GetAuthConfigs(), serverAddress) |
||||||
|
return c.file.Save() |
||||||
|
} |
||||||
|
|
||||||
|
// Get retrieves credentials for a specific server from the file store.
|
||||||
|
func (c *fileStore) Get(serverAddress string) (types.AuthConfig, error) { |
||||||
|
authConfig, ok := c.file.GetAuthConfigs()[serverAddress] |
||||||
|
if !ok { |
||||||
|
// Maybe they have a legacy config file, we will iterate the keys converting
|
||||||
|
// them to the new format and testing
|
||||||
|
for r, ac := range c.file.GetAuthConfigs() { |
||||||
|
if serverAddress == ConvertToHostname(r) { |
||||||
|
return ac, nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
authConfig = types.AuthConfig{} |
||||||
|
} |
||||||
|
return authConfig, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (c *fileStore) GetAll() (map[string]types.AuthConfig, error) { |
||||||
|
return c.file.GetAuthConfigs(), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Store saves the given credentials in the file store.
|
||||||
|
func (c *fileStore) Store(authConfig types.AuthConfig) error { |
||||||
|
c.file.GetAuthConfigs()[authConfig.ServerAddress] = authConfig |
||||||
|
return c.file.Save() |
||||||
|
} |
||||||
|
|
||||||
|
func (c *fileStore) GetFilename() string { |
||||||
|
return c.file.GetFilename() |
||||||
|
} |
||||||
|
|
||||||
|
func (c *fileStore) IsFileStore() bool { |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// ConvertToHostname converts a registry url which has http|https prepended
|
||||||
|
// to just an hostname.
|
||||||
|
// Copied from github.com/docker/docker/registry.ConvertToHostname to reduce dependencies.
|
||||||
|
func ConvertToHostname(url string) string { |
||||||
|
stripped := url |
||||||
|
if strings.HasPrefix(url, "http://") { |
||||||
|
stripped = strings.TrimPrefix(url, "http://") |
||||||
|
} else if strings.HasPrefix(url, "https://") { |
||||||
|
stripped = strings.TrimPrefix(url, "https://") |
||||||
|
} |
||||||
|
|
||||||
|
nameParts := strings.SplitN(stripped, "/", 2) |
||||||
|
|
||||||
|
return nameParts[0] |
||||||
|
} |
@ -0,0 +1,143 @@ |
|||||||
|
package credentials |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/docker/cli/cli/config/types" |
||||||
|
"github.com/docker/docker-credential-helpers/client" |
||||||
|
"github.com/docker/docker-credential-helpers/credentials" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
remoteCredentialsPrefix = "docker-credential-" |
||||||
|
tokenUsername = "<token>" |
||||||
|
) |
||||||
|
|
||||||
|
// nativeStore implements a credentials store
|
||||||
|
// using native keychain to keep credentials secure.
|
||||||
|
// It piggybacks into a file store to keep users' emails.
|
||||||
|
type nativeStore struct { |
||||||
|
programFunc client.ProgramFunc |
||||||
|
fileStore Store |
||||||
|
} |
||||||
|
|
||||||
|
// NewNativeStore creates a new native store that
|
||||||
|
// uses a remote helper program to manage credentials.
|
||||||
|
func NewNativeStore(file store, helperSuffix string) Store { |
||||||
|
name := remoteCredentialsPrefix + helperSuffix |
||||||
|
return &nativeStore{ |
||||||
|
programFunc: client.NewShellProgramFunc(name), |
||||||
|
fileStore: NewFileStore(file), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Erase removes the given credentials from the native store.
|
||||||
|
func (c *nativeStore) Erase(serverAddress string) error { |
||||||
|
if err := client.Erase(c.programFunc, serverAddress); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Fallback to plain text store to remove email
|
||||||
|
return c.fileStore.Erase(serverAddress) |
||||||
|
} |
||||||
|
|
||||||
|
// Get retrieves credentials for a specific server from the native store.
|
||||||
|
func (c *nativeStore) Get(serverAddress string) (types.AuthConfig, error) { |
||||||
|
// load user email if it exist or an empty auth config.
|
||||||
|
auth, _ := c.fileStore.Get(serverAddress) |
||||||
|
|
||||||
|
creds, err := c.getCredentialsFromStore(serverAddress) |
||||||
|
if err != nil { |
||||||
|
return auth, err |
||||||
|
} |
||||||
|
auth.Username = creds.Username |
||||||
|
auth.IdentityToken = creds.IdentityToken |
||||||
|
auth.Password = creds.Password |
||||||
|
|
||||||
|
return auth, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetAll retrieves all the credentials from the native store.
|
||||||
|
func (c *nativeStore) GetAll() (map[string]types.AuthConfig, error) { |
||||||
|
auths, err := c.listCredentialsInStore() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Emails are only stored in the file store.
|
||||||
|
// This call can be safely eliminated when emails are removed.
|
||||||
|
fileConfigs, _ := c.fileStore.GetAll() |
||||||
|
|
||||||
|
authConfigs := make(map[string]types.AuthConfig) |
||||||
|
for registry := range auths { |
||||||
|
creds, err := c.getCredentialsFromStore(registry) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
ac := fileConfigs[registry] // might contain Email
|
||||||
|
ac.Username = creds.Username |
||||||
|
ac.Password = creds.Password |
||||||
|
ac.IdentityToken = creds.IdentityToken |
||||||
|
authConfigs[registry] = ac |
||||||
|
} |
||||||
|
|
||||||
|
return authConfigs, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Store saves the given credentials in the file store.
|
||||||
|
func (c *nativeStore) Store(authConfig types.AuthConfig) error { |
||||||
|
if err := c.storeCredentialsInStore(authConfig); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
authConfig.Username = "" |
||||||
|
authConfig.Password = "" |
||||||
|
authConfig.IdentityToken = "" |
||||||
|
|
||||||
|
// Fallback to old credential in plain text to save only the email
|
||||||
|
return c.fileStore.Store(authConfig) |
||||||
|
} |
||||||
|
|
||||||
|
// storeCredentialsInStore executes the command to store the credentials in the native store.
|
||||||
|
func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error { |
||||||
|
creds := &credentials.Credentials{ |
||||||
|
ServerURL: config.ServerAddress, |
||||||
|
Username: config.Username, |
||||||
|
Secret: config.Password, |
||||||
|
} |
||||||
|
|
||||||
|
if config.IdentityToken != "" { |
||||||
|
creds.Username = tokenUsername |
||||||
|
creds.Secret = config.IdentityToken |
||||||
|
} |
||||||
|
|
||||||
|
return client.Store(c.programFunc, creds) |
||||||
|
} |
||||||
|
|
||||||
|
// getCredentialsFromStore executes the command to get the credentials from the native store.
|
||||||
|
func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthConfig, error) { |
||||||
|
var ret types.AuthConfig |
||||||
|
|
||||||
|
creds, err := client.Get(c.programFunc, serverAddress) |
||||||
|
if err != nil { |
||||||
|
if credentials.IsErrCredentialsNotFound(err) { |
||||||
|
// do not return an error if the credentials are not
|
||||||
|
// in the keychain. Let docker ask for new credentials.
|
||||||
|
return ret, nil |
||||||
|
} |
||||||
|
return ret, err |
||||||
|
} |
||||||
|
|
||||||
|
if creds.Username == tokenUsername { |
||||||
|
ret.IdentityToken = creds.Secret |
||||||
|
} else { |
||||||
|
ret.Password = creds.Secret |
||||||
|
ret.Username = creds.Username |
||||||
|
} |
||||||
|
|
||||||
|
ret.ServerAddress = serverAddress |
||||||
|
return ret, nil |
||||||
|
} |
||||||
|
|
||||||
|
// listCredentialsInStore returns a listing of stored credentials as a map of
|
||||||
|
// URL -> username.
|
||||||
|
func (c *nativeStore) listCredentialsInStore() (map[string]string, error) { |
||||||
|
return client.List(c.programFunc) |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
package types |
||||||
|
|
||||||
|
// AuthConfig contains authorization information for connecting to a Registry
|
||||||
|
type AuthConfig struct { |
||||||
|
Username string `json:"username,omitempty"` |
||||||
|
Password string `json:"password,omitempty"` |
||||||
|
Auth string `json:"auth,omitempty"` |
||||||
|
|
||||||
|
// Email is an optional value associated with the username.
|
||||||
|
// This field is deprecated and will be removed in a later
|
||||||
|
// version of docker.
|
||||||
|
Email string `json:"email,omitempty"` |
||||||
|
|
||||||
|
ServerAddress string `json:"serveraddress,omitempty"` |
||||||
|
|
||||||
|
// IdentityToken is used to authenticate the user and get
|
||||||
|
// an access token for the registry.
|
||||||
|
IdentityToken string `json:"identitytoken,omitempty"` |
||||||
|
|
||||||
|
// RegistryToken is a bearer token to be sent to a registry
|
||||||
|
RegistryToken string `json:"registrytoken,omitempty"` |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
package docker |
||||||
|
|
||||||
|
const ( |
||||||
|
// DockerEndpoint is the name of the docker endpoint in a stored context
|
||||||
|
DockerEndpoint = "docker" |
||||||
|
) |
@ -0,0 +1,168 @@ |
|||||||
|
package docker |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/tls" |
||||||
|
"crypto/x509" |
||||||
|
"encoding/pem" |
||||||
|
"fmt" |
||||||
|
"net" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/docker/cli/cli/connhelper" |
||||||
|
"github.com/docker/cli/cli/context" |
||||||
|
"github.com/docker/cli/cli/context/store" |
||||||
|
"github.com/docker/docker/client" |
||||||
|
"github.com/docker/go-connections/tlsconfig" |
||||||
|
"github.com/pkg/errors" |
||||||
|
) |
||||||
|
|
||||||
|
// EndpointMeta is a typed wrapper around a context-store generic endpoint describing
|
||||||
|
// a Docker Engine endpoint, without its tls config
|
||||||
|
type EndpointMeta = context.EndpointMetaBase |
||||||
|
|
||||||
|
// Endpoint is a typed wrapper around a context-store generic endpoint describing
|
||||||
|
// a Docker Engine endpoint, with its tls data
|
||||||
|
type Endpoint struct { |
||||||
|
EndpointMeta |
||||||
|
TLSData *context.TLSData |
||||||
|
TLSPassword string |
||||||
|
} |
||||||
|
|
||||||
|
// WithTLSData loads TLS materials for the endpoint
|
||||||
|
func WithTLSData(s store.Reader, contextName string, m EndpointMeta) (Endpoint, error) { |
||||||
|
tlsData, err := context.LoadTLSData(s, contextName, DockerEndpoint) |
||||||
|
if err != nil { |
||||||
|
return Endpoint{}, err |
||||||
|
} |
||||||
|
return Endpoint{ |
||||||
|
EndpointMeta: m, |
||||||
|
TLSData: tlsData, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
// tlsConfig extracts a context docker endpoint TLS config
|
||||||
|
func (c *Endpoint) tlsConfig() (*tls.Config, error) { |
||||||
|
if c.TLSData == nil && !c.SkipTLSVerify { |
||||||
|
// there is no specific tls config
|
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
var tlsOpts []func(*tls.Config) |
||||||
|
if c.TLSData != nil && c.TLSData.CA != nil { |
||||||
|
certPool := x509.NewCertPool() |
||||||
|
if !certPool.AppendCertsFromPEM(c.TLSData.CA) { |
||||||
|
return nil, errors.New("failed to retrieve context tls info: ca.pem seems invalid") |
||||||
|
} |
||||||
|
tlsOpts = append(tlsOpts, func(cfg *tls.Config) { |
||||||
|
cfg.RootCAs = certPool |
||||||
|
}) |
||||||
|
} |
||||||
|
if c.TLSData != nil && c.TLSData.Key != nil && c.TLSData.Cert != nil { |
||||||
|
keyBytes := c.TLSData.Key |
||||||
|
pemBlock, _ := pem.Decode(keyBytes) |
||||||
|
if pemBlock == nil { |
||||||
|
return nil, fmt.Errorf("no valid private key found") |
||||||
|
} |
||||||
|
|
||||||
|
var err error |
||||||
|
if x509.IsEncryptedPEMBlock(pemBlock) { |
||||||
|
keyBytes, err = x509.DecryptPEMBlock(pemBlock, []byte(c.TLSPassword)) |
||||||
|
if err != nil { |
||||||
|
return nil, errors.Wrap(err, "private key is encrypted, but could not decrypt it") |
||||||
|
} |
||||||
|
keyBytes = pem.EncodeToMemory(&pem.Block{Type: pemBlock.Type, Bytes: keyBytes}) |
||||||
|
} |
||||||
|
|
||||||
|
x509cert, err := tls.X509KeyPair(c.TLSData.Cert, keyBytes) |
||||||
|
if err != nil { |
||||||
|
return nil, errors.Wrap(err, "failed to retrieve context tls info") |
||||||
|
} |
||||||
|
tlsOpts = append(tlsOpts, func(cfg *tls.Config) { |
||||||
|
cfg.Certificates = []tls.Certificate{x509cert} |
||||||
|
}) |
||||||
|
} |
||||||
|
if c.SkipTLSVerify { |
||||||
|
tlsOpts = append(tlsOpts, func(cfg *tls.Config) { |
||||||
|
cfg.InsecureSkipVerify = true |
||||||
|
}) |
||||||
|
} |
||||||
|
return tlsconfig.ClientDefault(tlsOpts...), nil |
||||||
|
} |
||||||
|
|
||||||
|
// ClientOpts returns a slice of Client options to configure an API client with this endpoint
|
||||||
|
func (c *Endpoint) ClientOpts() ([]client.Opt, error) { |
||||||
|
var result []client.Opt |
||||||
|
if c.Host != "" { |
||||||
|
helper, err := connhelper.GetConnectionHelper(c.Host) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if helper == nil { |
||||||
|
tlsConfig, err := c.tlsConfig() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
result = append(result, |
||||||
|
withHTTPClient(tlsConfig), |
||||||
|
client.WithHost(c.Host), |
||||||
|
) |
||||||
|
|
||||||
|
} else { |
||||||
|
httpClient := &http.Client{ |
||||||
|
// No tls
|
||||||
|
// No proxy
|
||||||
|
Transport: &http.Transport{ |
||||||
|
DialContext: helper.Dialer, |
||||||
|
}, |
||||||
|
} |
||||||
|
result = append(result, |
||||||
|
client.WithHTTPClient(httpClient), |
||||||
|
client.WithHost(helper.Host), |
||||||
|
client.WithDialContext(helper.Dialer), |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
version := os.Getenv("DOCKER_API_VERSION") |
||||||
|
if version != "" { |
||||||
|
result = append(result, client.WithVersion(version)) |
||||||
|
} else { |
||||||
|
result = append(result, client.WithAPIVersionNegotiation()) |
||||||
|
} |
||||||
|
return result, nil |
||||||
|
} |
||||||
|
|
||||||
|
func withHTTPClient(tlsConfig *tls.Config) func(*client.Client) error { |
||||||
|
return func(c *client.Client) error { |
||||||
|
if tlsConfig == nil { |
||||||
|
// Use the default HTTPClient
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
httpClient := &http.Client{ |
||||||
|
Transport: &http.Transport{ |
||||||
|
TLSClientConfig: tlsConfig, |
||||||
|
DialContext: (&net.Dialer{ |
||||||
|
KeepAlive: 30 * time.Second, |
||||||
|
Timeout: 30 * time.Second, |
||||||
|
}).DialContext, |
||||||
|
}, |
||||||
|
CheckRedirect: client.CheckRedirect, |
||||||
|
} |
||||||
|
return client.WithHTTPClient(httpClient)(c) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// EndpointFromContext parses a context docker endpoint metadata into a typed EndpointMeta structure
|
||||||
|
func EndpointFromContext(metadata store.Metadata) (EndpointMeta, error) { |
||||||
|
ep, ok := metadata.Endpoints[DockerEndpoint] |
||||||
|
if !ok { |
||||||
|
return EndpointMeta{}, errors.New("cannot find docker endpoint in context") |
||||||
|
} |
||||||
|
typed, ok := ep.(EndpointMeta) |
||||||
|
if !ok { |
||||||
|
return EndpointMeta{}, errors.Errorf("endpoint %q is not of type EndpointMeta", DockerEndpoint) |
||||||
|
} |
||||||
|
return typed, nil |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
package context |
||||||
|
|
||||||
|
// EndpointMetaBase contains fields we expect to be common for most context endpoints
|
||||||
|
type EndpointMetaBase struct { |
||||||
|
Host string `json:",omitempty"` |
||||||
|
SkipTLSVerify bool |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
// Package store provides a generic way to store credentials to connect to virtually any kind of remote system.
|
||||||
|
// The term `context` comes from the similar feature in Kubernetes kubectl config files.
|
||||||
|
//
|
||||||
|
// Conceptually, a context is a set of metadata and TLS data, that can be used to connect to various endpoints
|
||||||
|
// of a remote system. TLS data and metadata are stored separately, so that in the future, we will be able to store sensitive
|
||||||
|
// information in a more secure way, depending on the os we are running on (e.g.: on Windows we could use the user Certificate Store, on Mac OS the user Keychain...).
|
||||||
|
//
|
||||||
|
// Current implementation is purely file based with the following structure:
|
||||||
|
// ${CONTEXT_ROOT}
|
||||||
|
// - meta/
|
||||||
|
// - <context id>/meta.json: contains context medata (key/value pairs) as well as a list of endpoints (themselves containing key/value pair metadata)
|
||||||
|
// - tls/
|
||||||
|
// - <context id>/endpoint1/: directory containing TLS data for the endpoint1 in the corresponding context
|
||||||
|
//
|
||||||
|
// The context store itself has absolutely no knowledge about what a docker or a kubernetes endpoint should contain in term of metadata or TLS config.
|
||||||
|
// Client code is responsible for generating and parsing endpoint metadata and TLS files.
|
||||||
|
// The multi-endpoints approach of this package allows to combine many different endpoints in the same "context" (e.g., the Docker CLI
|
||||||
|
// is able for a single context to define both a docker endpoint and a Kubernetes endpoint for the same cluster, and also specify which
|
||||||
|
// orchestrator to use by default when deploying a compose stack on this cluster).
|
||||||
|
//
|
||||||
|
// Context IDs are actually SHA256 hashes of the context name, and are there only to avoid dealing with special characters in context names.
|
||||||
|
package store |
@ -0,0 +1,29 @@ |
|||||||
|
package store |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"io" |
||||||
|
) |
||||||
|
|
||||||
|
// LimitedReader is a fork of io.LimitedReader to override Read.
|
||||||
|
type LimitedReader struct { |
||||||
|
R io.Reader |
||||||
|
N int64 // max bytes remaining
|
||||||
|
} |
||||||
|
|
||||||
|
// Read is a fork of io.LimitedReader.Read that returns an error when limit exceeded.
|
||||||
|
func (l *LimitedReader) Read(p []byte) (n int, err error) { |
||||||
|
if l.N < 0 { |
||||||
|
return 0, errors.New("read exceeds the defined limit") |
||||||
|
} |
||||||
|
if l.N == 0 { |
||||||
|
return 0, io.EOF |
||||||
|
} |
||||||
|
// have to cap N + 1 otherwise we won't hit limit err
|
||||||
|
if int64(len(p)) > l.N+1 { |
||||||
|
p = p[0 : l.N+1] |
||||||
|
} |
||||||
|
n, err = l.R.Read(p) |
||||||
|
l.N -= int64(n) |
||||||
|
return n, err |
||||||
|
} |
@ -0,0 +1,153 @@ |
|||||||
|
package store |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"reflect" |
||||||
|
"sort" |
||||||
|
|
||||||
|
"github.com/fvbommel/sortorder" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
metadataDir = "meta" |
||||||
|
metaFile = "meta.json" |
||||||
|
) |
||||||
|
|
||||||
|
type metadataStore struct { |
||||||
|
root string |
||||||
|
config Config |
||||||
|
} |
||||||
|
|
||||||
|
func (s *metadataStore) contextDir(id contextdir) string { |
||||||
|
return filepath.Join(s.root, string(id)) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *metadataStore) createOrUpdate(meta Metadata) error { |
||||||
|
contextDir := s.contextDir(contextdirOf(meta.Name)) |
||||||
|
if err := os.MkdirAll(contextDir, 0755); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
bytes, err := json.Marshal(&meta) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return ioutil.WriteFile(filepath.Join(contextDir, metaFile), bytes, 0644) |
||||||
|
} |
||||||
|
|
||||||
|
func parseTypedOrMap(payload []byte, getter TypeGetter) (interface{}, error) { |
||||||
|
if len(payload) == 0 || string(payload) == "null" { |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
if getter == nil { |
||||||
|
var res map[string]interface{} |
||||||
|
if err := json.Unmarshal(payload, &res); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return res, nil |
||||||
|
} |
||||||
|
typed := getter() |
||||||
|
if err := json.Unmarshal(payload, typed); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return reflect.ValueOf(typed).Elem().Interface(), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *metadataStore) get(id contextdir) (Metadata, error) { |
||||||
|
contextDir := s.contextDir(id) |
||||||
|
bytes, err := ioutil.ReadFile(filepath.Join(contextDir, metaFile)) |
||||||
|
if err != nil { |
||||||
|
return Metadata{}, convertContextDoesNotExist(err) |
||||||
|
} |
||||||
|
var untyped untypedContextMetadata |
||||||
|
r := Metadata{ |
||||||
|
Endpoints: make(map[string]interface{}), |
||||||
|
} |
||||||
|
if err := json.Unmarshal(bytes, &untyped); err != nil { |
||||||
|
return Metadata{}, err |
||||||
|
} |
||||||
|
r.Name = untyped.Name |
||||||
|
if r.Metadata, err = parseTypedOrMap(untyped.Metadata, s.config.contextType); err != nil { |
||||||
|
return Metadata{}, err |
||||||
|
} |
||||||
|
for k, v := range untyped.Endpoints { |
||||||
|
if r.Endpoints[k], err = parseTypedOrMap(v, s.config.endpointTypes[k]); err != nil { |
||||||
|
return Metadata{}, err |
||||||
|
} |
||||||
|
} |
||||||
|
return r, err |
||||||
|
} |
||||||
|
|
||||||
|
func (s *metadataStore) remove(id contextdir) error { |
||||||
|
contextDir := s.contextDir(id) |
||||||
|
return os.RemoveAll(contextDir) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *metadataStore) list() ([]Metadata, error) { |
||||||
|
ctxDirs, err := listRecursivelyMetadataDirs(s.root) |
||||||
|
if err != nil { |
||||||
|
if os.IsNotExist(err) { |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
var res []Metadata |
||||||
|
for _, dir := range ctxDirs { |
||||||
|
c, err := s.get(contextdir(dir)) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
res = append(res, c) |
||||||
|
} |
||||||
|
sort.Slice(res, func(i, j int) bool { |
||||||
|
return sortorder.NaturalLess(res[i].Name, res[j].Name) |
||||||
|
}) |
||||||
|
return res, nil |
||||||
|
} |
||||||
|
|
||||||
|
func isContextDir(path string) bool { |
||||||
|
s, err := os.Stat(filepath.Join(path, metaFile)) |
||||||
|
if err != nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
return !s.IsDir() |
||||||
|
} |
||||||
|
|
||||||
|
func listRecursivelyMetadataDirs(root string) ([]string, error) { |
||||||
|
fis, err := ioutil.ReadDir(root) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
var result []string |
||||||
|
for _, fi := range fis { |
||||||
|
if fi.IsDir() { |
||||||
|
if isContextDir(filepath.Join(root, fi.Name())) { |
||||||
|
result = append(result, fi.Name()) |
||||||
|
} |
||||||
|
subs, err := listRecursivelyMetadataDirs(filepath.Join(root, fi.Name())) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
for _, s := range subs { |
||||||
|
result = append(result, fmt.Sprintf("%s/%s", fi.Name(), s)) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return result, nil |
||||||
|
} |
||||||
|
|
||||||
|
func convertContextDoesNotExist(err error) error { |
||||||
|
if os.IsNotExist(err) { |
||||||
|
return &contextDoesNotExistError{} |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
type untypedContextMetadata struct { |
||||||
|
Metadata json.RawMessage `json:"metadata,omitempty"` |
||||||
|
Endpoints map[string]json.RawMessage `json:"endpoints,omitempty"` |
||||||
|
Name string `json:"name,omitempty"` |
||||||
|
} |
@ -0,0 +1,540 @@ |
|||||||
|
package store |
||||||
|
|
||||||
|
import ( |
||||||
|
"archive/tar" |
||||||
|
"archive/zip" |
||||||
|
"bufio" |
||||||
|
"bytes" |
||||||
|
_ "crypto/sha256" // ensure ids can be computed
|
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"io/ioutil" |
||||||
|
"net/http" |
||||||
|
"path" |
||||||
|
"path/filepath" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/docker/docker/errdefs" |
||||||
|
digest "github.com/opencontainers/go-digest" |
||||||
|
"github.com/pkg/errors" |
||||||
|
) |
||||||
|
|
||||||
|
const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$" |
||||||
|
|
||||||
|
var restrictedNameRegEx = regexp.MustCompile(restrictedNamePattern) |
||||||
|
|
||||||
|
// Store provides a context store for easily remembering endpoints configuration
|
||||||
|
type Store interface { |
||||||
|
Reader |
||||||
|
Lister |
||||||
|
Writer |
||||||
|
StorageInfoProvider |
||||||
|
} |
||||||
|
|
||||||
|
// Reader provides read-only (without list) access to context data
|
||||||
|
type Reader interface { |
||||||
|
GetMetadata(name string) (Metadata, error) |
||||||
|
ListTLSFiles(name string) (map[string]EndpointFiles, error) |
||||||
|
GetTLSData(contextName, endpointName, fileName string) ([]byte, error) |
||||||
|
} |
||||||
|
|
||||||
|
// Lister provides listing of contexts
|
||||||
|
type Lister interface { |
||||||
|
List() ([]Metadata, error) |
||||||
|
} |
||||||
|
|
||||||
|
// ReaderLister combines Reader and Lister interfaces
|
||||||
|
type ReaderLister interface { |
||||||
|
Reader |
||||||
|
Lister |
||||||
|
} |
||||||
|
|
||||||
|
// StorageInfoProvider provides more information about storage details of contexts
|
||||||
|
type StorageInfoProvider interface { |
||||||
|
GetStorageInfo(contextName string) StorageInfo |
||||||
|
} |
||||||
|
|
||||||
|
// Writer provides write access to context data
|
||||||
|
type Writer interface { |
||||||
|
CreateOrUpdate(meta Metadata) error |
||||||
|
Remove(name string) error |
||||||
|
ResetTLSMaterial(name string, data *ContextTLSData) error |
||||||
|
ResetEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error |
||||||
|
} |
||||||
|
|
||||||
|
// ReaderWriter combines Reader and Writer interfaces
|
||||||
|
type ReaderWriter interface { |
||||||
|
Reader |
||||||
|
Writer |
||||||
|
} |
||||||
|
|
||||||
|
// Metadata contains metadata about a context and its endpoints
|
||||||
|
type Metadata struct { |
||||||
|
Name string `json:",omitempty"` |
||||||
|
Metadata interface{} `json:",omitempty"` |
||||||
|
Endpoints map[string]interface{} `json:",omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
// StorageInfo contains data about where a given context is stored
|
||||||
|
type StorageInfo struct { |
||||||
|
MetadataPath string |
||||||
|
TLSPath string |
||||||
|
} |
||||||
|
|
||||||
|
// EndpointTLSData represents tls data for a given endpoint
|
||||||
|
type EndpointTLSData struct { |
||||||
|
Files map[string][]byte |
||||||
|
} |
||||||
|
|
||||||
|
// ContextTLSData represents tls data for a whole context
|
||||||
|
type ContextTLSData struct { |
||||||
|
Endpoints map[string]EndpointTLSData |
||||||
|
} |
||||||
|
|
||||||
|
// New creates a store from a given directory.
|
||||||
|
// If the directory does not exist or is empty, initialize it
|
||||||
|
func New(dir string, cfg Config) Store { |
||||||
|
metaRoot := filepath.Join(dir, metadataDir) |
||||||
|
tlsRoot := filepath.Join(dir, tlsDir) |
||||||
|
|
||||||
|
return &store{ |
||||||
|
meta: &metadataStore{ |
||||||
|
root: metaRoot, |
||||||
|
config: cfg, |
||||||
|
}, |
||||||
|
tls: &tlsStore{ |
||||||
|
root: tlsRoot, |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type store struct { |
||||||
|
meta *metadataStore |
||||||
|
tls *tlsStore |
||||||
|
} |
||||||
|
|
||||||
|
func (s *store) List() ([]Metadata, error) { |
||||||
|
return s.meta.list() |
||||||
|
} |
||||||
|
|
||||||
|
func (s *store) CreateOrUpdate(meta Metadata) error { |
||||||
|
return s.meta.createOrUpdate(meta) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *store) Remove(name string) error { |
||||||
|
id := contextdirOf(name) |
||||||
|
if err := s.meta.remove(id); err != nil { |
||||||
|
return patchErrContextName(err, name) |
||||||
|
} |
||||||
|
return patchErrContextName(s.tls.removeAllContextData(id), name) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *store) GetMetadata(name string) (Metadata, error) { |
||||||
|
res, err := s.meta.get(contextdirOf(name)) |
||||||
|
patchErrContextName(err, name) |
||||||
|
return res, err |
||||||
|
} |
||||||
|
|
||||||
|
func (s *store) ResetTLSMaterial(name string, data *ContextTLSData) error { |
||||||
|
id := contextdirOf(name) |
||||||
|
if err := s.tls.removeAllContextData(id); err != nil { |
||||||
|
return patchErrContextName(err, name) |
||||||
|
} |
||||||
|
if data == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
for ep, files := range data.Endpoints { |
||||||
|
for fileName, data := range files.Files { |
||||||
|
if err := s.tls.createOrUpdate(id, ep, fileName, data); err != nil { |
||||||
|
return patchErrContextName(err, name) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *store) ResetEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error { |
||||||
|
id := contextdirOf(contextName) |
||||||
|
if err := s.tls.removeAllEndpointData(id, endpointName); err != nil { |
||||||
|
return patchErrContextName(err, contextName) |
||||||
|
} |
||||||
|
if data == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
for fileName, data := range data.Files { |
||||||
|
if err := s.tls.createOrUpdate(id, endpointName, fileName, data); err != nil { |
||||||
|
return patchErrContextName(err, contextName) |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *store) ListTLSFiles(name string) (map[string]EndpointFiles, error) { |
||||||
|
res, err := s.tls.listContextData(contextdirOf(name)) |
||||||
|
return res, patchErrContextName(err, name) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *store) GetTLSData(contextName, endpointName, fileName string) ([]byte, error) { |
||||||
|
res, err := s.tls.getData(contextdirOf(contextName), endpointName, fileName) |
||||||
|
return res, patchErrContextName(err, contextName) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *store) GetStorageInfo(contextName string) StorageInfo { |
||||||
|
dir := contextdirOf(contextName) |
||||||
|
return StorageInfo{ |
||||||
|
MetadataPath: s.meta.contextDir(dir), |
||||||
|
TLSPath: s.tls.contextDir(dir), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ValidateContextName checks a context name is valid.
|
||||||
|
func ValidateContextName(name string) error { |
||||||
|
if name == "" { |
||||||
|
return errors.New("context name cannot be empty") |
||||||
|
} |
||||||
|
if name == "default" { |
||||||
|
return errors.New(`"default" is a reserved context name`) |
||||||
|
} |
||||||
|
if !restrictedNameRegEx.MatchString(name) { |
||||||
|
return fmt.Errorf("context name %q is invalid, names are validated against regexp %q", name, restrictedNamePattern) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Export exports an existing namespace into an opaque data stream
|
||||||
|
// This stream is actually a tarball containing context metadata and TLS materials, but it does
|
||||||
|
// not map 1:1 the layout of the context store (don't try to restore it manually without calling store.Import)
|
||||||
|
func Export(name string, s Reader) io.ReadCloser { |
||||||
|
reader, writer := io.Pipe() |
||||||
|
go func() { |
||||||
|
tw := tar.NewWriter(writer) |
||||||
|
defer tw.Close() |
||||||
|
defer writer.Close() |
||||||
|
meta, err := s.GetMetadata(name) |
||||||
|
if err != nil { |
||||||
|
writer.CloseWithError(err) |
||||||
|
return |
||||||
|
} |
||||||
|
metaBytes, err := json.Marshal(&meta) |
||||||
|
if err != nil { |
||||||
|
writer.CloseWithError(err) |
||||||
|
return |
||||||
|
} |
||||||
|
if err = tw.WriteHeader(&tar.Header{ |
||||||
|
Name: metaFile, |
||||||
|
Mode: 0644, |
||||||
|
Size: int64(len(metaBytes)), |
||||||
|
}); err != nil { |
||||||
|
writer.CloseWithError(err) |
||||||
|
return |
||||||
|
} |
||||||
|
if _, err = tw.Write(metaBytes); err != nil { |
||||||
|
writer.CloseWithError(err) |
||||||
|
return |
||||||
|
} |
||||||
|
tlsFiles, err := s.ListTLSFiles(name) |
||||||
|
if err != nil { |
||||||
|
writer.CloseWithError(err) |
||||||
|
return |
||||||
|
} |
||||||
|
if err = tw.WriteHeader(&tar.Header{ |
||||||
|
Name: "tls", |
||||||
|
Mode: 0700, |
||||||
|
Size: 0, |
||||||
|
Typeflag: tar.TypeDir, |
||||||
|
}); err != nil { |
||||||
|
writer.CloseWithError(err) |
||||||
|
return |
||||||
|
} |
||||||
|
for endpointName, endpointFiles := range tlsFiles { |
||||||
|
if err = tw.WriteHeader(&tar.Header{ |
||||||
|
Name: path.Join("tls", endpointName), |
||||||
|
Mode: 0700, |
||||||
|
Size: 0, |
||||||
|
Typeflag: tar.TypeDir, |
||||||
|
}); err != nil { |
||||||
|
writer.CloseWithError(err) |
||||||
|
return |
||||||
|
} |
||||||
|
for _, fileName := range endpointFiles { |
||||||
|
data, err := s.GetTLSData(name, endpointName, fileName) |
||||||
|
if err != nil { |
||||||
|
writer.CloseWithError(err) |
||||||
|
return |
||||||
|
} |
||||||
|
if err = tw.WriteHeader(&tar.Header{ |
||||||
|
Name: path.Join("tls", endpointName, fileName), |
||||||
|
Mode: 0600, |
||||||
|
Size: int64(len(data)), |
||||||
|
}); err != nil { |
||||||
|
writer.CloseWithError(err) |
||||||
|
return |
||||||
|
} |
||||||
|
if _, err = tw.Write(data); err != nil { |
||||||
|
writer.CloseWithError(err) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
return reader |
||||||
|
} |
||||||
|
|
||||||
|
const ( |
||||||
|
maxAllowedFileSizeToImport int64 = 10 << 20 |
||||||
|
zipType string = "application/zip" |
||||||
|
) |
||||||
|
|
||||||
|
func getImportContentType(r *bufio.Reader) (string, error) { |
||||||
|
head, err := r.Peek(512) |
||||||
|
if err != nil && err != io.EOF { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return http.DetectContentType(head), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Import imports an exported context into a store
|
||||||
|
func Import(name string, s Writer, reader io.Reader) error { |
||||||
|
// Buffered reader will not advance the buffer, needed to determine content type
|
||||||
|
r := bufio.NewReader(reader) |
||||||
|
|
||||||
|
importContentType, err := getImportContentType(r) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
switch importContentType { |
||||||
|
case zipType: |
||||||
|
return importZip(name, s, r) |
||||||
|
default: |
||||||
|
// Assume it's a TAR (TAR does not have a "magic number")
|
||||||
|
return importTar(name, s, r) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func isValidFilePath(p string) error { |
||||||
|
if p != metaFile && !strings.HasPrefix(p, "tls/") { |
||||||
|
return errors.New("unexpected context file") |
||||||
|
} |
||||||
|
if path.Clean(p) != p { |
||||||
|
return errors.New("unexpected path format") |
||||||
|
} |
||||||
|
if strings.Contains(p, `\`) { |
||||||
|
return errors.New(`unexpected '\' in path`) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func importTar(name string, s Writer, reader io.Reader) error { |
||||||
|
tr := tar.NewReader(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport}) |
||||||
|
tlsData := ContextTLSData{ |
||||||
|
Endpoints: map[string]EndpointTLSData{}, |
||||||
|
} |
||||||
|
var importedMetaFile bool |
||||||
|
for { |
||||||
|
hdr, err := tr.Next() |
||||||
|
if err == io.EOF { |
||||||
|
break |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if hdr.Typeflag != tar.TypeReg { |
||||||
|
// skip this entry, only taking files into account
|
||||||
|
continue |
||||||
|
} |
||||||
|
if err := isValidFilePath(hdr.Name); err != nil { |
||||||
|
return errors.Wrap(err, hdr.Name) |
||||||
|
} |
||||||
|
if hdr.Name == metaFile { |
||||||
|
data, err := ioutil.ReadAll(tr) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
meta, err := parseMetadata(data, name) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := s.CreateOrUpdate(meta); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
importedMetaFile = true |
||||||
|
} else if strings.HasPrefix(hdr.Name, "tls/") { |
||||||
|
data, err := ioutil.ReadAll(tr) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := importEndpointTLS(&tlsData, hdr.Name, data); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if !importedMetaFile { |
||||||
|
return errdefs.InvalidParameter(errors.New("invalid context: no metadata found")) |
||||||
|
} |
||||||
|
return s.ResetTLSMaterial(name, &tlsData) |
||||||
|
} |
||||||
|
|
||||||
|
func importZip(name string, s Writer, reader io.Reader) error { |
||||||
|
body, err := ioutil.ReadAll(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport}) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
zr, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
tlsData := ContextTLSData{ |
||||||
|
Endpoints: map[string]EndpointTLSData{}, |
||||||
|
} |
||||||
|
|
||||||
|
var importedMetaFile bool |
||||||
|
for _, zf := range zr.File { |
||||||
|
fi := zf.FileInfo() |
||||||
|
if !fi.Mode().IsRegular() { |
||||||
|
// skip this entry, only taking regular files into account
|
||||||
|
continue |
||||||
|
} |
||||||
|
if err := isValidFilePath(zf.Name); err != nil { |
||||||
|
return errors.Wrap(err, zf.Name) |
||||||
|
} |
||||||
|
if zf.Name == metaFile { |
||||||
|
f, err := zf.Open() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(&LimitedReader{R: f, N: maxAllowedFileSizeToImport}) |
||||||
|
defer f.Close() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
meta, err := parseMetadata(data, name) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := s.CreateOrUpdate(meta); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
importedMetaFile = true |
||||||
|
} else if strings.HasPrefix(zf.Name, "tls/") { |
||||||
|
f, err := zf.Open() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
data, err := ioutil.ReadAll(f) |
||||||
|
defer f.Close() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
err = importEndpointTLS(&tlsData, zf.Name, data) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if !importedMetaFile { |
||||||
|
return errdefs.InvalidParameter(errors.New("invalid context: no metadata found")) |
||||||
|
} |
||||||
|
return s.ResetTLSMaterial(name, &tlsData) |
||||||
|
} |
||||||
|
|
||||||
|
func parseMetadata(data []byte, name string) (Metadata, error) { |
||||||
|
var meta Metadata |
||||||
|
if err := json.Unmarshal(data, &meta); err != nil { |
||||||
|
return meta, err |
||||||
|
} |
||||||
|
if err := ValidateContextName(name); err != nil { |
||||||
|
return Metadata{}, err |
||||||
|
} |
||||||
|
meta.Name = name |
||||||
|
return meta, nil |
||||||
|
} |
||||||
|
|
||||||
|
func importEndpointTLS(tlsData *ContextTLSData, path string, data []byte) error { |
||||||
|
parts := strings.SplitN(strings.TrimPrefix(path, "tls/"), "/", 2) |
||||||
|
if len(parts) != 2 { |
||||||
|
// TLS endpoints require archived file directory with 2 layers
|
||||||
|
// i.e. tls/{endpointName}/{fileName}
|
||||||
|
return errors.New("archive format is invalid") |
||||||
|
} |
||||||
|
|
||||||
|
epName := parts[0] |
||||||
|
fileName := parts[1] |
||||||
|
if _, ok := tlsData.Endpoints[epName]; !ok { |
||||||
|
tlsData.Endpoints[epName] = EndpointTLSData{ |
||||||
|
Files: map[string][]byte{}, |
||||||
|
} |
||||||
|
} |
||||||
|
tlsData.Endpoints[epName].Files[fileName] = data |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
type setContextName interface { |
||||||
|
setContext(name string) |
||||||
|
} |
||||||
|
|
||||||
|
type contextDoesNotExistError struct { |
||||||
|
name string |
||||||
|
} |
||||||
|
|
||||||
|
func (e *contextDoesNotExistError) Error() string { |
||||||
|
return fmt.Sprintf("context %q does not exist", e.name) |
||||||
|
} |
||||||
|
|
||||||
|
func (e *contextDoesNotExistError) setContext(name string) { |
||||||
|
e.name = name |
||||||
|
} |
||||||
|
|
||||||
|
// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
|
||||||
|
func (e *contextDoesNotExistError) NotFound() {} |
||||||
|
|
||||||
|
type tlsDataDoesNotExist interface { |
||||||
|
errdefs.ErrNotFound |
||||||
|
IsTLSDataDoesNotExist() |
||||||
|
} |
||||||
|
|
||||||
|
type tlsDataDoesNotExistError struct { |
||||||
|
context, endpoint, file string |
||||||
|
} |
||||||
|
|
||||||
|
func (e *tlsDataDoesNotExistError) Error() string { |
||||||
|
return fmt.Sprintf("tls data for %s/%s/%s does not exist", e.context, e.endpoint, e.file) |
||||||
|
} |
||||||
|
|
||||||
|
func (e *tlsDataDoesNotExistError) setContext(name string) { |
||||||
|
e.context = name |
||||||
|
} |
||||||
|
|
||||||
|
// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
|
||||||
|
func (e *tlsDataDoesNotExistError) NotFound() {} |
||||||
|
|
||||||
|
// IsTLSDataDoesNotExist satisfies tlsDataDoesNotExist
|
||||||
|
func (e *tlsDataDoesNotExistError) IsTLSDataDoesNotExist() {} |
||||||
|
|
||||||
|
// IsErrContextDoesNotExist checks if the given error is a "context does not exist" condition
|
||||||
|
func IsErrContextDoesNotExist(err error) bool { |
||||||
|
_, ok := err.(*contextDoesNotExistError) |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
// IsErrTLSDataDoesNotExist checks if the given error is a "context does not exist" condition
|
||||||
|
func IsErrTLSDataDoesNotExist(err error) bool { |
||||||
|
_, ok := err.(tlsDataDoesNotExist) |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
type contextdir string |
||||||
|
|
||||||
|
func contextdirOf(name string) contextdir { |
||||||
|
return contextdir(digest.FromString(name).Encoded()) |
||||||
|
} |
||||||
|
|
||||||
|
func patchErrContextName(err error, name string) error { |
||||||
|
if typed, ok := err.(setContextName); ok { |
||||||
|
typed.setContext(name) |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
package store |
||||||
|
|
||||||
|
// TypeGetter is a func used to determine the concrete type of a context or
|
||||||
|
// endpoint metadata by returning a pointer to an instance of the object
|
||||||
|
// eg: for a context of type DockerContext, the corresponding TypeGetter should return new(DockerContext)
|
||||||
|
type TypeGetter func() interface{} |
||||||
|
|
||||||
|
// NamedTypeGetter is a TypeGetter associated with a name
|
||||||
|
type NamedTypeGetter struct { |
||||||
|
name string |
||||||
|
typeGetter TypeGetter |
||||||
|
} |
||||||
|
|
||||||
|
// EndpointTypeGetter returns a NamedTypeGetter with the spcecified name and getter
|
||||||
|
func EndpointTypeGetter(name string, getter TypeGetter) NamedTypeGetter { |
||||||
|
return NamedTypeGetter{ |
||||||
|
name: name, |
||||||
|
typeGetter: getter, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Config is used to configure the metadata marshaler of the context store
|
||||||
|
type Config struct { |
||||||
|
contextType TypeGetter |
||||||
|
endpointTypes map[string]TypeGetter |
||||||
|
} |
||||||
|
|
||||||
|
// SetEndpoint set an endpoint typing information
|
||||||
|
func (c Config) SetEndpoint(name string, getter TypeGetter) { |
||||||
|
c.endpointTypes[name] = getter |
||||||
|
} |
||||||
|
|
||||||
|
// ForeachEndpointType calls cb on every endpoint type registered with the Config
|
||||||
|
func (c Config) ForeachEndpointType(cb func(string, TypeGetter) error) error { |
||||||
|
for n, ep := range c.endpointTypes { |
||||||
|
if err := cb(n, ep); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// NewConfig creates a config object
|
||||||
|
func NewConfig(contextType TypeGetter, endpoints ...NamedTypeGetter) Config { |
||||||
|
res := Config{ |
||||||
|
contextType: contextType, |
||||||
|
endpointTypes: make(map[string]TypeGetter), |
||||||
|
} |
||||||
|
for _, e := range endpoints { |
||||||
|
res.endpointTypes[e.name] = e.typeGetter |
||||||
|
} |
||||||
|
return res |
||||||
|
} |
@ -0,0 +1,99 @@ |
|||||||
|
package store |
||||||
|
|
||||||
|
import ( |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
) |
||||||
|
|
||||||
|
const tlsDir = "tls" |
||||||
|
|
||||||
|
type tlsStore struct { |
||||||
|
root string |
||||||
|
} |
||||||
|
|
||||||
|
func (s *tlsStore) contextDir(id contextdir) string { |
||||||
|
return filepath.Join(s.root, string(id)) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *tlsStore) endpointDir(contextID contextdir, name string) string { |
||||||
|
return filepath.Join(s.root, string(contextID), name) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *tlsStore) filePath(contextID contextdir, endpointName, filename string) string { |
||||||
|
return filepath.Join(s.root, string(contextID), endpointName, filename) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *tlsStore) createOrUpdate(contextID contextdir, endpointName, filename string, data []byte) error { |
||||||
|
epdir := s.endpointDir(contextID, endpointName) |
||||||
|
parentOfRoot := filepath.Dir(s.root) |
||||||
|
if err := os.MkdirAll(parentOfRoot, 0755); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := os.MkdirAll(epdir, 0700); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return ioutil.WriteFile(s.filePath(contextID, endpointName, filename), data, 0600) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *tlsStore) getData(contextID contextdir, endpointName, filename string) ([]byte, error) { |
||||||
|
data, err := ioutil.ReadFile(s.filePath(contextID, endpointName, filename)) |
||||||
|
if err != nil { |
||||||
|
return nil, convertTLSDataDoesNotExist(endpointName, filename, err) |
||||||
|
} |
||||||
|
return data, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *tlsStore) remove(contextID contextdir, endpointName, filename string) error { |
||||||
|
err := os.Remove(s.filePath(contextID, endpointName, filename)) |
||||||
|
if os.IsNotExist(err) { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func (s *tlsStore) removeAllEndpointData(contextID contextdir, endpointName string) error { |
||||||
|
return os.RemoveAll(s.endpointDir(contextID, endpointName)) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *tlsStore) removeAllContextData(contextID contextdir) error { |
||||||
|
return os.RemoveAll(s.contextDir(contextID)) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *tlsStore) listContextData(contextID contextdir) (map[string]EndpointFiles, error) { |
||||||
|
epFSs, err := ioutil.ReadDir(s.contextDir(contextID)) |
||||||
|
if err != nil { |
||||||
|
if os.IsNotExist(err) { |
||||||
|
return map[string]EndpointFiles{}, nil |
||||||
|
} |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
r := make(map[string]EndpointFiles) |
||||||
|
for _, epFS := range epFSs { |
||||||
|
if epFS.IsDir() { |
||||||
|
epDir := s.endpointDir(contextID, epFS.Name()) |
||||||
|
fss, err := ioutil.ReadDir(epDir) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
var files EndpointFiles |
||||||
|
for _, fs := range fss { |
||||||
|
if !fs.IsDir() { |
||||||
|
files = append(files, fs.Name()) |
||||||
|
} |
||||||
|
} |
||||||
|
r[epFS.Name()] = files |
||||||
|
} |
||||||
|
} |
||||||
|
return r, nil |
||||||
|
} |
||||||
|
|
||||||
|
// EndpointFiles is a slice of strings representing file names
|
||||||
|
type EndpointFiles []string |
||||||
|
|
||||||
|
func convertTLSDataDoesNotExist(endpoint, file string, err error) error { |
||||||
|
if os.IsNotExist(err) { |
||||||
|
return &tlsDataDoesNotExistError{endpoint: endpoint, file: file} |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
@ -0,0 +1,98 @@ |
|||||||
|
package context |
||||||
|
|
||||||
|
import ( |
||||||
|
"io/ioutil" |
||||||
|
|
||||||
|
"github.com/docker/cli/cli/context/store" |
||||||
|
"github.com/pkg/errors" |
||||||
|
"github.com/sirupsen/logrus" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
caKey = "ca.pem" |
||||||
|
certKey = "cert.pem" |
||||||
|
keyKey = "key.pem" |
||||||
|
) |
||||||
|
|
||||||
|
// TLSData holds ca/cert/key raw data
|
||||||
|
type TLSData struct { |
||||||
|
CA []byte |
||||||
|
Key []byte |
||||||
|
Cert []byte |
||||||
|
} |
||||||
|
|
||||||
|
// ToStoreTLSData converts TLSData to the store representation
|
||||||
|
func (data *TLSData) ToStoreTLSData() *store.EndpointTLSData { |
||||||
|
if data == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
result := store.EndpointTLSData{ |
||||||
|
Files: make(map[string][]byte), |
||||||
|
} |
||||||
|
if data.CA != nil { |
||||||
|
result.Files[caKey] = data.CA |
||||||
|
} |
||||||
|
if data.Cert != nil { |
||||||
|
result.Files[certKey] = data.Cert |
||||||
|
} |
||||||
|
if data.Key != nil { |
||||||
|
result.Files[keyKey] = data.Key |
||||||
|
} |
||||||
|
return &result |
||||||
|
} |
||||||
|
|
||||||
|
// LoadTLSData loads TLS data from the store
|
||||||
|
func LoadTLSData(s store.Reader, contextName, endpointName string) (*TLSData, error) { |
||||||
|
tlsFiles, err := s.ListTLSFiles(contextName) |
||||||
|
if err != nil { |
||||||
|
return nil, errors.Wrapf(err, "failed to retrieve context tls files for context %q", contextName) |
||||||
|
} |
||||||
|
if epTLSFiles, ok := tlsFiles[endpointName]; ok { |
||||||
|
var tlsData TLSData |
||||||
|
for _, f := range epTLSFiles { |
||||||
|
data, err := s.GetTLSData(contextName, endpointName, f) |
||||||
|
if err != nil { |
||||||
|
return nil, errors.Wrapf(err, "failed to retrieve context tls data for file %q of context %q", f, contextName) |
||||||
|
} |
||||||
|
switch f { |
||||||
|
case caKey: |
||||||
|
tlsData.CA = data |
||||||
|
case certKey: |
||||||
|
tlsData.Cert = data |
||||||
|
case keyKey: |
||||||
|
tlsData.Key = data |
||||||
|
default: |
||||||
|
logrus.Warnf("unknown file %s in context %s tls bundle", f, contextName) |
||||||
|
} |
||||||
|
} |
||||||
|
return &tlsData, nil |
||||||
|
} |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
|
||||||
|
// TLSDataFromFiles reads files into a TLSData struct (or returns nil if all paths are empty)
|
||||||
|
func TLSDataFromFiles(caPath, certPath, keyPath string) (*TLSData, error) { |
||||||
|
var ( |
||||||
|
ca, cert, key []byte |
||||||
|
err error |
||||||
|
) |
||||||
|
if caPath != "" { |
||||||
|
if ca, err = ioutil.ReadFile(caPath); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
if certPath != "" { |
||||||
|
if cert, err = ioutil.ReadFile(certPath); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
if keyPath != "" { |
||||||
|
if key, err = ioutil.ReadFile(keyPath); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
if ca == nil && cert == nil && key == nil { |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
return &TLSData{CA: ca, Cert: cert, Key: key}, nil |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
package debug |
||||||
|
|
||||||
|
import ( |
||||||
|
"os" |
||||||
|
|
||||||
|
"github.com/sirupsen/logrus" |
||||||
|
) |
||||||
|
|
||||||
|
// Enable sets the DEBUG env var to true
|
||||||
|
// and makes the logger to log at debug level.
|
||||||
|
func Enable() { |
||||||
|
os.Setenv("DEBUG", "1") |
||||||
|
logrus.SetLevel(logrus.DebugLevel) |
||||||
|
} |
||||||
|
|
||||||
|
// Disable sets the DEBUG env var to false
|
||||||
|
// and makes the logger to log at info level.
|
||||||
|
func Disable() { |
||||||
|
os.Setenv("DEBUG", "") |
||||||
|
logrus.SetLevel(logrus.InfoLevel) |
||||||
|
} |
||||||
|
|
||||||
|
// IsEnabled checks whether the debug flag is set or not.
|
||||||
|
func IsEnabled() bool { |
||||||
|
return os.Getenv("DEBUG") != "" |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
package flags |
||||||
|
|
||||||
|
// ClientOptions are the options used to configure the client cli
|
||||||
|
type ClientOptions struct { |
||||||
|
Common *CommonOptions |
||||||
|
ConfigDir string |
||||||
|
} |
||||||
|
|
||||||
|
// NewClientOptions returns a new ClientOptions
|
||||||
|
func NewClientOptions() *ClientOptions { |
||||||
|
return &ClientOptions{Common: NewCommonOptions()} |
||||||
|
} |
@ -0,0 +1,122 @@ |
|||||||
|
package flags |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
|
||||||
|
cliconfig "github.com/docker/cli/cli/config" |
||||||
|
"github.com/docker/cli/opts" |
||||||
|
"github.com/docker/go-connections/tlsconfig" |
||||||
|
"github.com/sirupsen/logrus" |
||||||
|
"github.com/spf13/pflag" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
// DefaultCaFile is the default filename for the CA pem file
|
||||||
|
DefaultCaFile = "ca.pem" |
||||||
|
// DefaultKeyFile is the default filename for the key pem file
|
||||||
|
DefaultKeyFile = "key.pem" |
||||||
|
// DefaultCertFile is the default filename for the cert pem file
|
||||||
|
DefaultCertFile = "cert.pem" |
||||||
|
// FlagTLSVerify is the flag name for the TLS verification option
|
||||||
|
FlagTLSVerify = "tlsverify" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
dockerCertPath = os.Getenv("DOCKER_CERT_PATH") |
||||||
|
dockerTLSVerify = os.Getenv("DOCKER_TLS_VERIFY") != "" |
||||||
|
dockerTLS = os.Getenv("DOCKER_TLS") != "" |
||||||
|
) |
||||||
|
|
||||||
|
// CommonOptions are options common to both the client and the daemon.
|
||||||
|
type CommonOptions struct { |
||||||
|
Debug bool |
||||||
|
Hosts []string |
||||||
|
LogLevel string |
||||||
|
TLS bool |
||||||
|
TLSVerify bool |
||||||
|
TLSOptions *tlsconfig.Options |
||||||
|
Context string |
||||||
|
} |
||||||
|
|
||||||
|
// NewCommonOptions returns a new CommonOptions
|
||||||
|
func NewCommonOptions() *CommonOptions { |
||||||
|
return &CommonOptions{} |
||||||
|
} |
||||||
|
|
||||||
|
// InstallFlags adds flags for the common options on the FlagSet
|
||||||
|
func (commonOpts *CommonOptions) InstallFlags(flags *pflag.FlagSet) { |
||||||
|
if dockerCertPath == "" { |
||||||
|
dockerCertPath = cliconfig.Dir() |
||||||
|
} |
||||||
|
|
||||||
|
flags.BoolVarP(&commonOpts.Debug, "debug", "D", false, "Enable debug mode") |
||||||
|
flags.StringVarP(&commonOpts.LogLevel, "log-level", "l", "info", `Set the logging level ("debug"|"info"|"warn"|"error"|"fatal")`) |
||||||
|
flags.BoolVar(&commonOpts.TLS, "tls", dockerTLS, "Use TLS; implied by --tlsverify") |
||||||
|
flags.BoolVar(&commonOpts.TLSVerify, FlagTLSVerify, dockerTLSVerify, "Use TLS and verify the remote") |
||||||
|
|
||||||
|
// TODO use flag flags.String("identity"}, "i", "", "Path to libtrust key file")
|
||||||
|
|
||||||
|
commonOpts.TLSOptions = &tlsconfig.Options{ |
||||||
|
CAFile: filepath.Join(dockerCertPath, DefaultCaFile), |
||||||
|
CertFile: filepath.Join(dockerCertPath, DefaultCertFile), |
||||||
|
KeyFile: filepath.Join(dockerCertPath, DefaultKeyFile), |
||||||
|
} |
||||||
|
tlsOptions := commonOpts.TLSOptions |
||||||
|
flags.Var(opts.NewQuotedString(&tlsOptions.CAFile), "tlscacert", "Trust certs signed only by this CA") |
||||||
|
flags.Var(opts.NewQuotedString(&tlsOptions.CertFile), "tlscert", "Path to TLS certificate file") |
||||||
|
flags.Var(opts.NewQuotedString(&tlsOptions.KeyFile), "tlskey", "Path to TLS key file") |
||||||
|
|
||||||
|
// opts.ValidateHost is not used here, so as to allow connection helpers
|
||||||
|
hostOpt := opts.NewNamedListOptsRef("hosts", &commonOpts.Hosts, nil) |
||||||
|
flags.VarP(hostOpt, "host", "H", "Daemon socket(s) to connect to") |
||||||
|
flags.StringVarP(&commonOpts.Context, "context", "c", "", |
||||||
|
`Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with "docker context use")`) |
||||||
|
} |
||||||
|
|
||||||
|
// SetDefaultOptions sets default values for options after flag parsing is
|
||||||
|
// complete
|
||||||
|
func (commonOpts *CommonOptions) SetDefaultOptions(flags *pflag.FlagSet) { |
||||||
|
// Regardless of whether the user sets it to true or false, if they
|
||||||
|
// specify --tlsverify at all then we need to turn on TLS
|
||||||
|
// TLSVerify can be true even if not set due to DOCKER_TLS_VERIFY env var, so we need
|
||||||
|
// to check that here as well
|
||||||
|
if flags.Changed(FlagTLSVerify) || commonOpts.TLSVerify { |
||||||
|
commonOpts.TLS = true |
||||||
|
} |
||||||
|
|
||||||
|
if !commonOpts.TLS { |
||||||
|
commonOpts.TLSOptions = nil |
||||||
|
} else { |
||||||
|
tlsOptions := commonOpts.TLSOptions |
||||||
|
tlsOptions.InsecureSkipVerify = !commonOpts.TLSVerify |
||||||
|
|
||||||
|
// Reset CertFile and KeyFile to empty string if the user did not specify
|
||||||
|
// the respective flags and the respective default files were not found.
|
||||||
|
if !flags.Changed("tlscert") { |
||||||
|
if _, err := os.Stat(tlsOptions.CertFile); os.IsNotExist(err) { |
||||||
|
tlsOptions.CertFile = "" |
||||||
|
} |
||||||
|
} |
||||||
|
if !flags.Changed("tlskey") { |
||||||
|
if _, err := os.Stat(tlsOptions.KeyFile); os.IsNotExist(err) { |
||||||
|
tlsOptions.KeyFile = "" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// SetLogLevel sets the logrus logging level
|
||||||
|
func SetLogLevel(logLevel string) { |
||||||
|
if logLevel != "" { |
||||||
|
lvl, err := logrus.ParseLevel(logLevel) |
||||||
|
if err != nil { |
||||||
|
fmt.Fprintf(os.Stderr, "Unable to parse logging level: %s\n", logLevel) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
logrus.SetLevel(lvl) |
||||||
|
} else { |
||||||
|
logrus.SetLevel(logrus.InfoLevel) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,180 @@ |
|||||||
|
package store |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/docker/cli/cli/manifest/types" |
||||||
|
"github.com/docker/distribution/manifest/manifestlist" |
||||||
|
"github.com/docker/distribution/reference" |
||||||
|
digest "github.com/opencontainers/go-digest" |
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
||||||
|
"github.com/pkg/errors" |
||||||
|
) |
||||||
|
|
||||||
|
// Store manages local storage of image distribution manifests
|
||||||
|
type Store interface { |
||||||
|
Remove(listRef reference.Reference) error |
||||||
|
Get(listRef reference.Reference, manifest reference.Reference) (types.ImageManifest, error) |
||||||
|
GetList(listRef reference.Reference) ([]types.ImageManifest, error) |
||||||
|
Save(listRef reference.Reference, manifest reference.Reference, image types.ImageManifest) error |
||||||
|
} |
||||||
|
|
||||||
|
// fsStore manages manifest files stored on the local filesystem
|
||||||
|
type fsStore struct { |
||||||
|
root string |
||||||
|
} |
||||||
|
|
||||||
|
// NewStore returns a new store for a local file path
|
||||||
|
func NewStore(root string) Store { |
||||||
|
return &fsStore{root: root} |
||||||
|
} |
||||||
|
|
||||||
|
// Remove a manifest list from local storage
|
||||||
|
func (s *fsStore) Remove(listRef reference.Reference) error { |
||||||
|
path := filepath.Join(s.root, makeFilesafeName(listRef.String())) |
||||||
|
return os.RemoveAll(path) |
||||||
|
} |
||||||
|
|
||||||
|
// Get returns the local manifest
|
||||||
|
func (s *fsStore) Get(listRef reference.Reference, manifest reference.Reference) (types.ImageManifest, error) { |
||||||
|
filename := manifestToFilename(s.root, listRef.String(), manifest.String()) |
||||||
|
return s.getFromFilename(manifest, filename) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *fsStore) getFromFilename(ref reference.Reference, filename string) (types.ImageManifest, error) { |
||||||
|
bytes, err := ioutil.ReadFile(filename) |
||||||
|
switch { |
||||||
|
case os.IsNotExist(err): |
||||||
|
return types.ImageManifest{}, newNotFoundError(ref.String()) |
||||||
|
case err != nil: |
||||||
|
return types.ImageManifest{}, err |
||||||
|
} |
||||||
|
var manifestInfo struct { |
||||||
|
types.ImageManifest |
||||||
|
|
||||||
|
// Deprecated Fields, replaced by Descriptor
|
||||||
|
Digest digest.Digest |
||||||
|
Platform *manifestlist.PlatformSpec |
||||||
|
} |
||||||
|
|
||||||
|
if err := json.Unmarshal(bytes, &manifestInfo); err != nil { |
||||||
|
return types.ImageManifest{}, err |
||||||
|
} |
||||||
|
|
||||||
|
// Compatibility with image manifests created before
|
||||||
|
// descriptor, newer versions omit Digest and Platform
|
||||||
|
if manifestInfo.Digest != "" { |
||||||
|
mediaType, raw, err := manifestInfo.Payload() |
||||||
|
if err != nil { |
||||||
|
return types.ImageManifest{}, err |
||||||
|
} |
||||||
|
if dgst := digest.FromBytes(raw); dgst != manifestInfo.Digest { |
||||||
|
return types.ImageManifest{}, errors.Errorf("invalid manifest file %v: image manifest digest mismatch (%v != %v)", filename, manifestInfo.Digest, dgst) |
||||||
|
} |
||||||
|
manifestInfo.ImageManifest.Descriptor = ocispec.Descriptor{ |
||||||
|
Digest: manifestInfo.Digest, |
||||||
|
Size: int64(len(raw)), |
||||||
|
MediaType: mediaType, |
||||||
|
Platform: types.OCIPlatform(manifestInfo.Platform), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return manifestInfo.ImageManifest, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetList returns all the local manifests for a transaction
|
||||||
|
func (s *fsStore) GetList(listRef reference.Reference) ([]types.ImageManifest, error) { |
||||||
|
filenames, err := s.listManifests(listRef.String()) |
||||||
|
switch { |
||||||
|
case err != nil: |
||||||
|
return nil, err |
||||||
|
case filenames == nil: |
||||||
|
return nil, newNotFoundError(listRef.String()) |
||||||
|
} |
||||||
|
|
||||||
|
manifests := []types.ImageManifest{} |
||||||
|
for _, filename := range filenames { |
||||||
|
filename = filepath.Join(s.root, makeFilesafeName(listRef.String()), filename) |
||||||
|
manifest, err := s.getFromFilename(listRef, filename) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
manifests = append(manifests, manifest) |
||||||
|
} |
||||||
|
return manifests, nil |
||||||
|
} |
||||||
|
|
||||||
|
// listManifests stored in a transaction
|
||||||
|
func (s *fsStore) listManifests(transaction string) ([]string, error) { |
||||||
|
transactionDir := filepath.Join(s.root, makeFilesafeName(transaction)) |
||||||
|
fileInfos, err := ioutil.ReadDir(transactionDir) |
||||||
|
switch { |
||||||
|
case os.IsNotExist(err): |
||||||
|
return nil, nil |
||||||
|
case err != nil: |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
filenames := []string{} |
||||||
|
for _, info := range fileInfos { |
||||||
|
filenames = append(filenames, info.Name()) |
||||||
|
} |
||||||
|
return filenames, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Save a manifest as part of a local manifest list
|
||||||
|
func (s *fsStore) Save(listRef reference.Reference, manifest reference.Reference, image types.ImageManifest) error { |
||||||
|
if err := s.createManifestListDirectory(listRef.String()); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
filename := manifestToFilename(s.root, listRef.String(), manifest.String()) |
||||||
|
bytes, err := json.Marshal(image) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return ioutil.WriteFile(filename, bytes, 0644) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *fsStore) createManifestListDirectory(transaction string) error { |
||||||
|
path := filepath.Join(s.root, makeFilesafeName(transaction)) |
||||||
|
return os.MkdirAll(path, 0755) |
||||||
|
} |
||||||
|
|
||||||
|
func manifestToFilename(root, manifestList, manifest string) string { |
||||||
|
return filepath.Join(root, makeFilesafeName(manifestList), makeFilesafeName(manifest)) |
||||||
|
} |
||||||
|
|
||||||
|
func makeFilesafeName(ref string) string { |
||||||
|
fileName := strings.Replace(ref, ":", "-", -1) |
||||||
|
return strings.Replace(fileName, "/", "_", -1) |
||||||
|
} |
||||||
|
|
||||||
|
type notFoundError struct { |
||||||
|
object string |
||||||
|
} |
||||||
|
|
||||||
|
func newNotFoundError(ref string) *notFoundError { |
||||||
|
return ¬FoundError{object: ref} |
||||||
|
} |
||||||
|
|
||||||
|
func (n *notFoundError) Error() string { |
||||||
|
return fmt.Sprintf("No such manifest: %s", n.object) |
||||||
|
} |
||||||
|
|
||||||
|
// NotFound interface
|
||||||
|
func (n *notFoundError) NotFound() {} |
||||||
|
|
||||||
|
// IsNotFound returns true if the error is a not found error
|
||||||
|
func IsNotFound(err error) bool { |
||||||
|
_, ok := err.(notFound) |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
type notFound interface { |
||||||
|
NotFound() |
||||||
|
} |
@ -0,0 +1,114 @@ |
|||||||
|
package types |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
|
||||||
|
"github.com/docker/distribution" |
||||||
|
"github.com/docker/distribution/manifest/manifestlist" |
||||||
|
"github.com/docker/distribution/manifest/schema2" |
||||||
|
"github.com/docker/distribution/reference" |
||||||
|
"github.com/opencontainers/go-digest" |
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
||||||
|
"github.com/pkg/errors" |
||||||
|
) |
||||||
|
|
||||||
|
// ImageManifest contains info to output for a manifest object.
|
||||||
|
type ImageManifest struct { |
||||||
|
Ref *SerializableNamed |
||||||
|
Descriptor ocispec.Descriptor |
||||||
|
|
||||||
|
// SchemaV2Manifest is used for inspection
|
||||||
|
// TODO: Deprecate this and store manifest blobs
|
||||||
|
SchemaV2Manifest *schema2.DeserializedManifest `json:",omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
// OCIPlatform creates an OCI platform from a manifest list platform spec
|
||||||
|
func OCIPlatform(ps *manifestlist.PlatformSpec) *ocispec.Platform { |
||||||
|
if ps == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return &ocispec.Platform{ |
||||||
|
Architecture: ps.Architecture, |
||||||
|
OS: ps.OS, |
||||||
|
OSVersion: ps.OSVersion, |
||||||
|
OSFeatures: ps.OSFeatures, |
||||||
|
Variant: ps.Variant, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// PlatformSpecFromOCI creates a platform spec from OCI platform
|
||||||
|
func PlatformSpecFromOCI(p *ocispec.Platform) *manifestlist.PlatformSpec { |
||||||
|
if p == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return &manifestlist.PlatformSpec{ |
||||||
|
Architecture: p.Architecture, |
||||||
|
OS: p.OS, |
||||||
|
OSVersion: p.OSVersion, |
||||||
|
OSFeatures: p.OSFeatures, |
||||||
|
Variant: p.Variant, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Blobs returns the digests for all the blobs referenced by this manifest
|
||||||
|
func (i ImageManifest) Blobs() []digest.Digest { |
||||||
|
digests := []digest.Digest{} |
||||||
|
for _, descriptor := range i.SchemaV2Manifest.References() { |
||||||
|
digests = append(digests, descriptor.Digest) |
||||||
|
} |
||||||
|
return digests |
||||||
|
} |
||||||
|
|
||||||
|
// Payload returns the media type and bytes for the manifest
|
||||||
|
func (i ImageManifest) Payload() (string, []byte, error) { |
||||||
|
// TODO: If available, read content from a content store by digest
|
||||||
|
switch { |
||||||
|
case i.SchemaV2Manifest != nil: |
||||||
|
return i.SchemaV2Manifest.Payload() |
||||||
|
default: |
||||||
|
return "", nil, errors.Errorf("%s has no payload", i.Ref) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// References implements the distribution.Manifest interface. It delegates to
|
||||||
|
// the underlying manifest.
|
||||||
|
func (i ImageManifest) References() []distribution.Descriptor { |
||||||
|
switch { |
||||||
|
case i.SchemaV2Manifest != nil: |
||||||
|
return i.SchemaV2Manifest.References() |
||||||
|
default: |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// NewImageManifest returns a new ImageManifest object. The values for Platform
|
||||||
|
// are initialized from those in the image
|
||||||
|
func NewImageManifest(ref reference.Named, desc ocispec.Descriptor, manifest *schema2.DeserializedManifest) ImageManifest { |
||||||
|
return ImageManifest{ |
||||||
|
Ref: &SerializableNamed{Named: ref}, |
||||||
|
Descriptor: desc, |
||||||
|
SchemaV2Manifest: manifest, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// SerializableNamed is a reference.Named that can be serialized and deserialized
|
||||||
|
// from JSON
|
||||||
|
type SerializableNamed struct { |
||||||
|
reference.Named |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalJSON loads the Named reference from JSON bytes
|
||||||
|
func (s *SerializableNamed) UnmarshalJSON(b []byte) error { |
||||||
|
var raw string |
||||||
|
if err := json.Unmarshal(b, &raw); err != nil { |
||||||
|
return errors.Wrapf(err, "invalid named reference bytes: %s", b) |
||||||
|
} |
||||||
|
var err error |
||||||
|
s.Named, err = reference.ParseNamed(raw) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// MarshalJSON returns the JSON bytes representation
|
||||||
|
func (s *SerializableNamed) MarshalJSON() ([]byte, error) { |
||||||
|
return json.Marshal(s.String()) |
||||||
|
} |
@ -0,0 +1,222 @@ |
|||||||
|
package client |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"strings" |
||||||
|
|
||||||
|
manifesttypes "github.com/docker/cli/cli/manifest/types" |
||||||
|
"github.com/docker/cli/cli/trust" |
||||||
|
"github.com/docker/distribution" |
||||||
|
"github.com/docker/distribution/reference" |
||||||
|
distributionclient "github.com/docker/distribution/registry/client" |
||||||
|
"github.com/docker/docker/api/types" |
||||||
|
registrytypes "github.com/docker/docker/api/types/registry" |
||||||
|
"github.com/opencontainers/go-digest" |
||||||
|
"github.com/pkg/errors" |
||||||
|
"github.com/sirupsen/logrus" |
||||||
|
) |
||||||
|
|
||||||
|
// RegistryClient is a client used to communicate with a Docker distribution
|
||||||
|
// registry
|
||||||
|
type RegistryClient interface { |
||||||
|
GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) |
||||||
|
GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) |
||||||
|
MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error |
||||||
|
PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) |
||||||
|
GetTags(ctx context.Context, ref reference.Named) ([]string, error) |
||||||
|
} |
||||||
|
|
||||||
|
// NewRegistryClient returns a new RegistryClient with a resolver
|
||||||
|
func NewRegistryClient(resolver AuthConfigResolver, userAgent string, insecure bool) RegistryClient { |
||||||
|
return &client{ |
||||||
|
authConfigResolver: resolver, |
||||||
|
insecureRegistry: insecure, |
||||||
|
userAgent: userAgent, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// AuthConfigResolver returns Auth Configuration for an index
|
||||||
|
type AuthConfigResolver func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig |
||||||
|
|
||||||
|
// PutManifestOptions is the data sent to push a manifest
|
||||||
|
type PutManifestOptions struct { |
||||||
|
MediaType string |
||||||
|
Payload []byte |
||||||
|
} |
||||||
|
|
||||||
|
type client struct { |
||||||
|
authConfigResolver AuthConfigResolver |
||||||
|
insecureRegistry bool |
||||||
|
userAgent string |
||||||
|
} |
||||||
|
|
||||||
|
// ErrBlobCreated returned when a blob mount request was created
|
||||||
|
type ErrBlobCreated struct { |
||||||
|
From reference.Named |
||||||
|
Target reference.Named |
||||||
|
} |
||||||
|
|
||||||
|
func (err ErrBlobCreated) Error() string { |
||||||
|
return fmt.Sprintf("blob mounted from: %v to: %v", |
||||||
|
err.From, err.Target) |
||||||
|
} |
||||||
|
|
||||||
|
// ErrHTTPProto returned if attempting to use TLS with a non-TLS registry
|
||||||
|
type ErrHTTPProto struct { |
||||||
|
OrigErr string |
||||||
|
} |
||||||
|
|
||||||
|
func (err ErrHTTPProto) Error() string { |
||||||
|
return err.OrigErr |
||||||
|
} |
||||||
|
|
||||||
|
var _ RegistryClient = &client{} |
||||||
|
|
||||||
|
// MountBlob into the registry, so it can be referenced by a manifest
|
||||||
|
func (c *client) MountBlob(ctx context.Context, sourceRef reference.Canonical, targetRef reference.Named) error { |
||||||
|
repoEndpoint, err := newDefaultRepositoryEndpoint(targetRef, c.insecureRegistry) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
repo, err := c.getRepositoryForReference(ctx, targetRef, repoEndpoint) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
lu, err := repo.Blobs(ctx).Create(ctx, distributionclient.WithMountFrom(sourceRef)) |
||||||
|
switch err.(type) { |
||||||
|
case distribution.ErrBlobMounted: |
||||||
|
logrus.Debugf("mount of blob %s succeeded", sourceRef) |
||||||
|
return nil |
||||||
|
case nil: |
||||||
|
default: |
||||||
|
return errors.Wrapf(err, "failed to mount blob %s to %s", sourceRef, targetRef) |
||||||
|
} |
||||||
|
lu.Cancel(ctx) |
||||||
|
logrus.Debugf("mount of blob %s created", sourceRef) |
||||||
|
return ErrBlobCreated{From: sourceRef, Target: targetRef} |
||||||
|
} |
||||||
|
|
||||||
|
// PutManifest sends the manifest to a registry and returns the new digest
|
||||||
|
func (c *client) PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) { |
||||||
|
repoEndpoint, err := newDefaultRepositoryEndpoint(ref, c.insecureRegistry) |
||||||
|
if err != nil { |
||||||
|
return digest.Digest(""), err |
||||||
|
} |
||||||
|
|
||||||
|
repo, err := c.getRepositoryForReference(ctx, ref, repoEndpoint) |
||||||
|
if err != nil { |
||||||
|
return digest.Digest(""), err |
||||||
|
} |
||||||
|
|
||||||
|
manifestService, err := repo.Manifests(ctx) |
||||||
|
if err != nil { |
||||||
|
return digest.Digest(""), err |
||||||
|
} |
||||||
|
|
||||||
|
_, opts, err := getManifestOptionsFromReference(ref) |
||||||
|
if err != nil { |
||||||
|
return digest.Digest(""), err |
||||||
|
} |
||||||
|
|
||||||
|
dgst, err := manifestService.Put(ctx, manifest, opts...) |
||||||
|
return dgst, errors.Wrapf(err, "failed to put manifest %s", ref) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *client) GetTags(ctx context.Context, ref reference.Named) ([]string, error) { |
||||||
|
repoEndpoint, err := newDefaultRepositoryEndpoint(ref, c.insecureRegistry) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
repo, err := c.getRepositoryForReference(ctx, ref, repoEndpoint) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return repo.Tags(ctx).All(ctx) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *client) getRepositoryForReference(ctx context.Context, ref reference.Named, repoEndpoint repositoryEndpoint) (distribution.Repository, error) { |
||||||
|
repoName, err := reference.WithName(repoEndpoint.Name()) |
||||||
|
if err != nil { |
||||||
|
return nil, errors.Wrapf(err, "failed to parse repo name from %s", ref) |
||||||
|
} |
||||||
|
httpTransport, err := c.getHTTPTransportForRepoEndpoint(ctx, repoEndpoint) |
||||||
|
if err != nil { |
||||||
|
if !strings.Contains(err.Error(), "server gave HTTP response to HTTPS client") { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if !repoEndpoint.endpoint.TLSConfig.InsecureSkipVerify { |
||||||
|
return nil, ErrHTTPProto{OrigErr: err.Error()} |
||||||
|
} |
||||||
|
// --insecure was set; fall back to plain HTTP
|
||||||
|
if url := repoEndpoint.endpoint.URL; url != nil && url.Scheme == "https" { |
||||||
|
url.Scheme = "http" |
||||||
|
httpTransport, err = c.getHTTPTransportForRepoEndpoint(ctx, repoEndpoint) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return distributionclient.NewRepository(repoName, repoEndpoint.BaseURL(), httpTransport) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *client) getHTTPTransportForRepoEndpoint(ctx context.Context, repoEndpoint repositoryEndpoint) (http.RoundTripper, error) { |
||||||
|
httpTransport, err := getHTTPTransport( |
||||||
|
c.authConfigResolver(ctx, repoEndpoint.info.Index), |
||||||
|
repoEndpoint.endpoint, |
||||||
|
repoEndpoint.Name(), |
||||||
|
c.userAgent) |
||||||
|
return httpTransport, errors.Wrap(err, "failed to configure transport") |
||||||
|
} |
||||||
|
|
||||||
|
// GetManifest returns an ImageManifest for the reference
|
||||||
|
func (c *client) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) { |
||||||
|
var result manifesttypes.ImageManifest |
||||||
|
fetch := func(ctx context.Context, repo distribution.Repository, ref reference.Named) (bool, error) { |
||||||
|
var err error |
||||||
|
result, err = fetchManifest(ctx, repo, ref) |
||||||
|
return result.Ref != nil, err |
||||||
|
} |
||||||
|
|
||||||
|
err := c.iterateEndpoints(ctx, ref, fetch) |
||||||
|
return result, err |
||||||
|
} |
||||||
|
|
||||||
|
// GetManifestList returns a list of ImageManifest for the reference
|
||||||
|
func (c *client) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) { |
||||||
|
result := []manifesttypes.ImageManifest{} |
||||||
|
fetch := func(ctx context.Context, repo distribution.Repository, ref reference.Named) (bool, error) { |
||||||
|
var err error |
||||||
|
result, err = fetchList(ctx, repo, ref) |
||||||
|
return len(result) > 0, err |
||||||
|
} |
||||||
|
|
||||||
|
err := c.iterateEndpoints(ctx, ref, fetch) |
||||||
|
return result, err |
||||||
|
} |
||||||
|
|
||||||
|
func getManifestOptionsFromReference(ref reference.Named) (digest.Digest, []distribution.ManifestServiceOption, error) { |
||||||
|
if tagged, isTagged := ref.(reference.NamedTagged); isTagged { |
||||||
|
tag := tagged.Tag() |
||||||
|
return "", []distribution.ManifestServiceOption{distribution.WithTag(tag)}, nil |
||||||
|
} |
||||||
|
if digested, isDigested := ref.(reference.Canonical); isDigested { |
||||||
|
return digested.Digest(), []distribution.ManifestServiceOption{}, nil |
||||||
|
} |
||||||
|
return "", nil, errors.Errorf("%s no tag or digest", ref) |
||||||
|
} |
||||||
|
|
||||||
|
// GetRegistryAuth returns the auth config given an input image
|
||||||
|
func GetRegistryAuth(ctx context.Context, resolver AuthConfigResolver, imageName string) (*types.AuthConfig, error) { |
||||||
|
distributionRef, err := reference.ParseNormalizedNamed(imageName) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("Failed to parse image name: %s: %s", imageName, err) |
||||||
|
} |
||||||
|
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, nil, resolver, distributionRef.String()) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("Failed to get imgRefAndAuth: %s", err) |
||||||
|
} |
||||||
|
return imgRefAndAuth.AuthConfig(), nil |
||||||
|
} |
@ -0,0 +1,133 @@ |
|||||||
|
package client |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net" |
||||||
|
"net/http" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/docker/distribution/reference" |
||||||
|
"github.com/docker/distribution/registry/client/auth" |
||||||
|
"github.com/docker/distribution/registry/client/transport" |
||||||
|
authtypes "github.com/docker/docker/api/types" |
||||||
|
"github.com/docker/docker/registry" |
||||||
|
"github.com/pkg/errors" |
||||||
|
) |
||||||
|
|
||||||
|
type repositoryEndpoint struct { |
||||||
|
info *registry.RepositoryInfo |
||||||
|
endpoint registry.APIEndpoint |
||||||
|
} |
||||||
|
|
||||||
|
// Name returns the repository name
|
||||||
|
func (r repositoryEndpoint) Name() string { |
||||||
|
repoName := r.info.Name.Name() |
||||||
|
// If endpoint does not support CanonicalName, use the RemoteName instead
|
||||||
|
if r.endpoint.TrimHostname { |
||||||
|
repoName = reference.Path(r.info.Name) |
||||||
|
} |
||||||
|
return repoName |
||||||
|
} |
||||||
|
|
||||||
|
// BaseURL returns the endpoint url
|
||||||
|
func (r repositoryEndpoint) BaseURL() string { |
||||||
|
return r.endpoint.URL.String() |
||||||
|
} |
||||||
|
|
||||||
|
func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) { |
||||||
|
repoInfo, err := registry.ParseRepositoryInfo(ref) |
||||||
|
if err != nil { |
||||||
|
return repositoryEndpoint{}, err |
||||||
|
} |
||||||
|
endpoint, err := getDefaultEndpointFromRepoInfo(repoInfo) |
||||||
|
if err != nil { |
||||||
|
return repositoryEndpoint{}, err |
||||||
|
} |
||||||
|
if insecure { |
||||||
|
endpoint.TLSConfig.InsecureSkipVerify = true |
||||||
|
} |
||||||
|
return repositoryEndpoint{info: repoInfo, endpoint: endpoint}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func getDefaultEndpointFromRepoInfo(repoInfo *registry.RepositoryInfo) (registry.APIEndpoint, error) { |
||||||
|
var err error |
||||||
|
|
||||||
|
options := registry.ServiceOptions{} |
||||||
|
registryService, err := registry.NewService(options) |
||||||
|
if err != nil { |
||||||
|
return registry.APIEndpoint{}, err |
||||||
|
} |
||||||
|
endpoints, err := registryService.LookupPushEndpoints(reference.Domain(repoInfo.Name)) |
||||||
|
if err != nil { |
||||||
|
return registry.APIEndpoint{}, err |
||||||
|
} |
||||||
|
// Default to the highest priority endpoint to return
|
||||||
|
endpoint := endpoints[0] |
||||||
|
if !repoInfo.Index.Secure { |
||||||
|
for _, ep := range endpoints { |
||||||
|
if ep.URL.Scheme == "http" { |
||||||
|
endpoint = ep |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return endpoint, nil |
||||||
|
} |
||||||
|
|
||||||
|
// getHTTPTransport builds a transport for use in communicating with a registry
|
||||||
|
func getHTTPTransport(authConfig authtypes.AuthConfig, endpoint registry.APIEndpoint, repoName string, userAgent string) (http.RoundTripper, error) { |
||||||
|
// get the http transport, this will be used in a client to upload manifest
|
||||||
|
base := &http.Transport{ |
||||||
|
Proxy: http.ProxyFromEnvironment, |
||||||
|
Dial: (&net.Dialer{ |
||||||
|
Timeout: 30 * time.Second, |
||||||
|
KeepAlive: 30 * time.Second, |
||||||
|
DualStack: true, |
||||||
|
}).Dial, |
||||||
|
TLSHandshakeTimeout: 10 * time.Second, |
||||||
|
TLSClientConfig: endpoint.TLSConfig, |
||||||
|
DisableKeepAlives: true, |
||||||
|
} |
||||||
|
|
||||||
|
modifiers := registry.Headers(userAgent, http.Header{}) |
||||||
|
authTransport := transport.NewTransport(base, modifiers...) |
||||||
|
challengeManager, confirmedV2, err := registry.PingV2Registry(endpoint.URL, authTransport) |
||||||
|
if err != nil { |
||||||
|
return nil, errors.Wrap(err, "error pinging v2 registry") |
||||||
|
} |
||||||
|
if !confirmedV2 { |
||||||
|
return nil, fmt.Errorf("unsupported registry version") |
||||||
|
} |
||||||
|
if authConfig.RegistryToken != "" { |
||||||
|
passThruTokenHandler := &existingTokenHandler{token: authConfig.RegistryToken} |
||||||
|
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler)) |
||||||
|
} else { |
||||||
|
creds := registry.NewStaticCredentialStore(&authConfig) |
||||||
|
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, "push", "pull") |
||||||
|
basicHandler := auth.NewBasicHandler(creds) |
||||||
|
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) |
||||||
|
} |
||||||
|
return transport.NewTransport(base, modifiers...), nil |
||||||
|
} |
||||||
|
|
||||||
|
// RepoNameForReference returns the repository name from a reference
|
||||||
|
func RepoNameForReference(ref reference.Named) (string, error) { |
||||||
|
// insecure is fine since this only returns the name
|
||||||
|
repo, err := newDefaultRepositoryEndpoint(ref, false) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
return repo.Name(), nil |
||||||
|
} |
||||||
|
|
||||||
|
type existingTokenHandler struct { |
||||||
|
token string |
||||||
|
} |
||||||
|
|
||||||
|
func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { |
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.token)) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (th *existingTokenHandler) Scheme() string { |
||||||
|
return "bearer" |
||||||
|
} |
@ -0,0 +1,304 @@ |
|||||||
|
package client |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/docker/cli/cli/manifest/types" |
||||||
|
"github.com/docker/distribution" |
||||||
|
"github.com/docker/distribution/manifest/manifestlist" |
||||||
|
"github.com/docker/distribution/manifest/schema2" |
||||||
|
"github.com/docker/distribution/reference" |
||||||
|
"github.com/docker/distribution/registry/api/errcode" |
||||||
|
v2 "github.com/docker/distribution/registry/api/v2" |
||||||
|
distclient "github.com/docker/distribution/registry/client" |
||||||
|
"github.com/docker/docker/registry" |
||||||
|
digest "github.com/opencontainers/go-digest" |
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
||||||
|
"github.com/pkg/errors" |
||||||
|
"github.com/sirupsen/logrus" |
||||||
|
) |
||||||
|
|
||||||
|
// fetchManifest pulls a manifest from a registry and returns it. An error
|
||||||
|
// is returned if no manifest is found matching namedRef.
|
||||||
|
func fetchManifest(ctx context.Context, repo distribution.Repository, ref reference.Named) (types.ImageManifest, error) { |
||||||
|
manifest, err := getManifest(ctx, repo, ref) |
||||||
|
if err != nil { |
||||||
|
return types.ImageManifest{}, err |
||||||
|
} |
||||||
|
|
||||||
|
switch v := manifest.(type) { |
||||||
|
// Removed Schema 1 support
|
||||||
|
case *schema2.DeserializedManifest: |
||||||
|
imageManifest, err := pullManifestSchemaV2(ctx, ref, repo, *v) |
||||||
|
if err != nil { |
||||||
|
return types.ImageManifest{}, err |
||||||
|
} |
||||||
|
return imageManifest, nil |
||||||
|
case *manifestlist.DeserializedManifestList: |
||||||
|
return types.ImageManifest{}, errors.Errorf("%s is a manifest list", ref) |
||||||
|
} |
||||||
|
return types.ImageManifest{}, errors.Errorf("%s is not a manifest", ref) |
||||||
|
} |
||||||
|
|
||||||
|
func fetchList(ctx context.Context, repo distribution.Repository, ref reference.Named) ([]types.ImageManifest, error) { |
||||||
|
manifest, err := getManifest(ctx, repo, ref) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
switch v := manifest.(type) { |
||||||
|
case *manifestlist.DeserializedManifestList: |
||||||
|
imageManifests, err := pullManifestList(ctx, ref, repo, *v) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return imageManifests, nil |
||||||
|
default: |
||||||
|
return nil, errors.Errorf("unsupported manifest format: %v", v) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func getManifest(ctx context.Context, repo distribution.Repository, ref reference.Named) (distribution.Manifest, error) { |
||||||
|
manSvc, err := repo.Manifests(ctx) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
dgst, opts, err := getManifestOptionsFromReference(ref) |
||||||
|
if err != nil { |
||||||
|
return nil, errors.Errorf("image manifest for %q does not exist", ref) |
||||||
|
} |
||||||
|
return manSvc.Get(ctx, dgst, opts...) |
||||||
|
} |
||||||
|
|
||||||
|
func pullManifestSchemaV2(ctx context.Context, ref reference.Named, repo distribution.Repository, mfst schema2.DeserializedManifest) (types.ImageManifest, error) { |
||||||
|
manifestDesc, err := validateManifestDigest(ref, mfst) |
||||||
|
if err != nil { |
||||||
|
return types.ImageManifest{}, err |
||||||
|
} |
||||||
|
configJSON, err := pullManifestSchemaV2ImageConfig(ctx, mfst.Target().Digest, repo) |
||||||
|
if err != nil { |
||||||
|
return types.ImageManifest{}, err |
||||||
|
} |
||||||
|
|
||||||
|
if manifestDesc.Platform == nil { |
||||||
|
manifestDesc.Platform = &ocispec.Platform{} |
||||||
|
} |
||||||
|
|
||||||
|
// Fill in os and architecture fields from config JSON
|
||||||
|
if err := json.Unmarshal(configJSON, manifestDesc.Platform); err != nil { |
||||||
|
return types.ImageManifest{}, err |
||||||
|
} |
||||||
|
|
||||||
|
return types.NewImageManifest(ref, manifestDesc, &mfst), nil |
||||||
|
} |
||||||
|
|
||||||
|
func pullManifestSchemaV2ImageConfig(ctx context.Context, dgst digest.Digest, repo distribution.Repository) ([]byte, error) { |
||||||
|
blobs := repo.Blobs(ctx) |
||||||
|
configJSON, err := blobs.Get(ctx, dgst) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
verifier := dgst.Verifier() |
||||||
|
if _, err := verifier.Write(configJSON); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if !verifier.Verified() { |
||||||
|
return nil, errors.Errorf("image config verification failed for digest %s", dgst) |
||||||
|
} |
||||||
|
return configJSON, nil |
||||||
|
} |
||||||
|
|
||||||
|
// validateManifestDigest computes the manifest digest, and, if pulling by
|
||||||
|
// digest, ensures that it matches the requested digest.
|
||||||
|
func validateManifestDigest(ref reference.Named, mfst distribution.Manifest) (ocispec.Descriptor, error) { |
||||||
|
mediaType, canonical, err := mfst.Payload() |
||||||
|
if err != nil { |
||||||
|
return ocispec.Descriptor{}, err |
||||||
|
} |
||||||
|
desc := ocispec.Descriptor{ |
||||||
|
Digest: digest.FromBytes(canonical), |
||||||
|
Size: int64(len(canonical)), |
||||||
|
MediaType: mediaType, |
||||||
|
} |
||||||
|
|
||||||
|
// If pull by digest, then verify the manifest digest.
|
||||||
|
if digested, isDigested := ref.(reference.Canonical); isDigested { |
||||||
|
if digested.Digest() != desc.Digest { |
||||||
|
err := fmt.Errorf("manifest verification failed for digest %s", digested.Digest()) |
||||||
|
return ocispec.Descriptor{}, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return desc, nil |
||||||
|
} |
||||||
|
|
||||||
|
// pullManifestList handles "manifest lists" which point to various
|
||||||
|
// platform-specific manifests.
|
||||||
|
func pullManifestList(ctx context.Context, ref reference.Named, repo distribution.Repository, mfstList manifestlist.DeserializedManifestList) ([]types.ImageManifest, error) { |
||||||
|
infos := []types.ImageManifest{} |
||||||
|
|
||||||
|
if _, err := validateManifestDigest(ref, mfstList); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
for _, manifestDescriptor := range mfstList.Manifests { |
||||||
|
manSvc, err := repo.Manifests(ctx) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
manifest, err := manSvc.Get(ctx, manifestDescriptor.Digest) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
v, ok := manifest.(*schema2.DeserializedManifest) |
||||||
|
if !ok { |
||||||
|
return nil, fmt.Errorf("unsupported manifest format: %v", v) |
||||||
|
} |
||||||
|
|
||||||
|
manifestRef, err := reference.WithDigest(ref, manifestDescriptor.Digest) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
imageManifest, err := pullManifestSchemaV2(ctx, manifestRef, repo, *v) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Replace platform from config
|
||||||
|
imageManifest.Descriptor.Platform = types.OCIPlatform(&manifestDescriptor.Platform) |
||||||
|
|
||||||
|
infos = append(infos, imageManifest) |
||||||
|
} |
||||||
|
return infos, nil |
||||||
|
} |
||||||
|
|
||||||
|
func continueOnError(err error) bool { |
||||||
|
switch v := err.(type) { |
||||||
|
case errcode.Errors: |
||||||
|
if len(v) == 0 { |
||||||
|
return true |
||||||
|
} |
||||||
|
return continueOnError(v[0]) |
||||||
|
case errcode.Error: |
||||||
|
e := err.(errcode.Error) |
||||||
|
switch e.Code { |
||||||
|
case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown: |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
case *distclient.UnexpectedHTTPResponseError: |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named, each func(context.Context, distribution.Repository, reference.Named) (bool, error)) error { |
||||||
|
endpoints, err := allEndpoints(namedRef, c.insecureRegistry) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
repoInfo, err := registry.ParseRepositoryInfo(namedRef) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
confirmedTLSRegistries := make(map[string]bool) |
||||||
|
for _, endpoint := range endpoints { |
||||||
|
if endpoint.Version == registry.APIVersion1 { |
||||||
|
logrus.Debugf("skipping v1 endpoint %s", endpoint.URL) |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if endpoint.URL.Scheme != "https" { |
||||||
|
if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS { |
||||||
|
logrus.Debugf("skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL) |
||||||
|
continue |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if c.insecureRegistry { |
||||||
|
endpoint.TLSConfig.InsecureSkipVerify = true |
||||||
|
} |
||||||
|
repoEndpoint := repositoryEndpoint{endpoint: endpoint, info: repoInfo} |
||||||
|
repo, err := c.getRepositoryForReference(ctx, namedRef, repoEndpoint) |
||||||
|
if err != nil { |
||||||
|
logrus.Debugf("error %s with repo endpoint %+v", err, repoEndpoint) |
||||||
|
if _, ok := err.(ErrHTTPProto); ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if endpoint.URL.Scheme == "http" && !c.insecureRegistry { |
||||||
|
logrus.Debugf("skipping non-tls registry endpoint: %s", endpoint.URL) |
||||||
|
continue |
||||||
|
} |
||||||
|
done, err := each(ctx, repo, namedRef) |
||||||
|
if err != nil { |
||||||
|
if continueOnError(err) { |
||||||
|
if endpoint.URL.Scheme == "https" { |
||||||
|
confirmedTLSRegistries[endpoint.URL.Host] = true |
||||||
|
} |
||||||
|
logrus.Debugf("continuing on error (%T) %s", err, err) |
||||||
|
continue |
||||||
|
} |
||||||
|
logrus.Debugf("not continuing on error (%T) %s", err, err) |
||||||
|
return err |
||||||
|
} |
||||||
|
if done { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
return newNotFoundError(namedRef.String()) |
||||||
|
} |
||||||
|
|
||||||
|
// allEndpoints returns a list of endpoints ordered by priority (v2, https, v1).
|
||||||
|
func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoint, error) { |
||||||
|
repoInfo, err := registry.ParseRepositoryInfo(namedRef) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var serviceOpts registry.ServiceOptions |
||||||
|
if insecure { |
||||||
|
logrus.Debugf("allowing insecure registry for: %s", reference.Domain(namedRef)) |
||||||
|
serviceOpts.InsecureRegistries = []string{reference.Domain(namedRef)} |
||||||
|
} |
||||||
|
registryService, err := registry.NewService(serviceOpts) |
||||||
|
if err != nil { |
||||||
|
return []registry.APIEndpoint{}, err |
||||||
|
} |
||||||
|
endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name)) |
||||||
|
logrus.Debugf("endpoints for %s: %v", namedRef, endpoints) |
||||||
|
return endpoints, err |
||||||
|
} |
||||||
|
|
||||||
|
type notFoundError struct { |
||||||
|
object string |
||||||
|
} |
||||||
|
|
||||||
|
func newNotFoundError(ref string) *notFoundError { |
||||||
|
return ¬FoundError{object: ref} |
||||||
|
} |
||||||
|
|
||||||
|
func (n *notFoundError) Error() string { |
||||||
|
return fmt.Sprintf("no such manifest: %s", n.object) |
||||||
|
} |
||||||
|
|
||||||
|
// NotFound interface
|
||||||
|
func (n *notFoundError) NotFound() {} |
||||||
|
|
||||||
|
// IsNotFound returns true if the error is a not found error
|
||||||
|
func IsNotFound(err error) bool { |
||||||
|
_, ok := err.(notFound) |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
type notFound interface { |
||||||
|
NotFound() |
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
package streams |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"runtime" |
||||||
|
|
||||||
|
"github.com/moby/term" |
||||||
|
) |
||||||
|
|
||||||
|
// In is an input stream used by the DockerCli to read user input
|
||||||
|
type In struct { |
||||||
|
commonStream |
||||||
|
in io.ReadCloser |
||||||
|
} |
||||||
|
|
||||||
|
func (i *In) Read(p []byte) (int, error) { |
||||||
|
return i.in.Read(p) |
||||||
|
} |
||||||
|
|
||||||
|
// Close implements the Closer interface
|
||||||
|
func (i *In) Close() error { |
||||||
|
return i.in.Close() |
||||||
|
} |
||||||
|
|
||||||
|
// SetRawTerminal sets raw mode on the input terminal
|
||||||
|
func (i *In) SetRawTerminal() (err error) { |
||||||
|
if os.Getenv("NORAW") != "" || !i.commonStream.isTerminal { |
||||||
|
return nil |
||||||
|
} |
||||||
|
i.commonStream.state, err = term.SetRawTerminal(i.commonStream.fd) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// CheckTty checks if we are trying to attach to a container tty
|
||||||
|
// from a non-tty client input stream, and if so, returns an error.
|
||||||
|
func (i *In) CheckTty(attachStdin, ttyMode bool) error { |
||||||
|
// In order to attach to a container tty, input stream for the client must
|
||||||
|
// be a tty itself: redirecting or piping the client standard input is
|
||||||
|
// incompatible with `docker run -t`, `docker exec -t` or `docker attach`.
|
||||||
|
if ttyMode && attachStdin && !i.isTerminal { |
||||||
|
eText := "the input device is not a TTY" |
||||||
|
if runtime.GOOS == "windows" { |
||||||
|
return errors.New(eText + ". If you are using mintty, try prefixing the command with 'winpty'") |
||||||
|
} |
||||||
|
return errors.New(eText) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// NewIn returns a new In object from a ReadCloser
|
||||||
|
func NewIn(in io.ReadCloser) *In { |
||||||
|
fd, isTerminal := term.GetFdInfo(in) |
||||||
|
return &In{commonStream: commonStream{fd: fd, isTerminal: isTerminal}, in: in} |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
package streams |
||||||
|
|
||||||
|
import ( |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
|
||||||
|
"github.com/moby/term" |
||||||
|
"github.com/sirupsen/logrus" |
||||||
|
) |
||||||
|
|
||||||
|
// Out is an output stream used by the DockerCli to write normal program
|
||||||
|
// output.
|
||||||
|
type Out struct { |
||||||
|
commonStream |
||||||
|
out io.Writer |
||||||
|
} |
||||||
|
|
||||||
|
func (o *Out) Write(p []byte) (int, error) { |
||||||
|
return o.out.Write(p) |
||||||
|
} |
||||||
|
|
||||||
|
// SetRawTerminal sets raw mode on the input terminal
|
||||||
|
func (o *Out) SetRawTerminal() (err error) { |
||||||
|
if os.Getenv("NORAW") != "" || !o.commonStream.isTerminal { |
||||||
|
return nil |
||||||
|
} |
||||||
|
o.commonStream.state, err = term.SetRawTerminalOutput(o.commonStream.fd) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// GetTtySize returns the height and width in characters of the tty
|
||||||
|
func (o *Out) GetTtySize() (uint, uint) { |
||||||
|
if !o.isTerminal { |
||||||
|
return 0, 0 |
||||||
|
} |
||||||
|
ws, err := term.GetWinsize(o.fd) |
||||||
|
if err != nil { |
||||||
|
logrus.Debugf("Error getting size: %s", err) |
||||||
|
if ws == nil { |
||||||
|
return 0, 0 |
||||||
|
} |
||||||
|
} |
||||||
|
return uint(ws.Height), uint(ws.Width) |
||||||
|
} |
||||||
|
|
||||||
|
// NewOut returns a new Out object from a Writer
|
||||||
|
func NewOut(out io.Writer) *Out { |
||||||
|
fd, isTerminal := term.GetFdInfo(out) |
||||||
|
return &Out{commonStream: commonStream{fd: fd, isTerminal: isTerminal}, out: out} |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
package streams |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/moby/term" |
||||||
|
) |
||||||
|
|
||||||
|
// commonStream is an input stream used by the DockerCli to read user input
|
||||||
|
type commonStream struct { |
||||||
|
fd uintptr |
||||||
|
isTerminal bool |
||||||
|
state *term.State |
||||||
|
} |
||||||
|
|
||||||
|
// FD returns the file descriptor number for this stream
|
||||||
|
func (s *commonStream) FD() uintptr { |
||||||
|
return s.fd |
||||||
|
} |
||||||
|
|
||||||
|
// IsTerminal returns true if this stream is connected to a terminal
|
||||||
|
func (s *commonStream) IsTerminal() bool { |
||||||
|
return s.isTerminal |
||||||
|
} |
||||||
|
|
||||||
|
// RestoreTerminal restores normal mode to the terminal
|
||||||
|
func (s *commonStream) RestoreTerminal() { |
||||||
|
if s.state != nil { |
||||||
|
term.RestoreTerminal(s.fd, s.state) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// SetIsTerminal sets the boolean used for isTerminal
|
||||||
|
func (s *commonStream) SetIsTerminal(isTerminal bool) { |
||||||
|
s.isTerminal = isTerminal |
||||||
|
} |
@ -0,0 +1,388 @@ |
|||||||
|
package trust |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"encoding/json" |
||||||
|
"io" |
||||||
|
"net" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"os" |
||||||
|
"path" |
||||||
|
"path/filepath" |
||||||
|
"time" |
||||||
|
|
||||||
|
cliconfig "github.com/docker/cli/cli/config" |
||||||
|
"github.com/docker/distribution/reference" |
||||||
|
"github.com/docker/distribution/registry/client/auth" |
||||||
|
"github.com/docker/distribution/registry/client/auth/challenge" |
||||||
|
"github.com/docker/distribution/registry/client/transport" |
||||||
|
"github.com/docker/docker/api/types" |
||||||
|
registrytypes "github.com/docker/docker/api/types/registry" |
||||||
|
"github.com/docker/docker/registry" |
||||||
|
"github.com/docker/go-connections/tlsconfig" |
||||||
|
digest "github.com/opencontainers/go-digest" |
||||||
|
"github.com/pkg/errors" |
||||||
|
"github.com/sirupsen/logrus" |
||||||
|
"github.com/theupdateframework/notary" |
||||||
|
"github.com/theupdateframework/notary/client" |
||||||
|
"github.com/theupdateframework/notary/passphrase" |
||||||
|
"github.com/theupdateframework/notary/storage" |
||||||
|
"github.com/theupdateframework/notary/trustmanager" |
||||||
|
"github.com/theupdateframework/notary/trustpinning" |
||||||
|
"github.com/theupdateframework/notary/tuf/data" |
||||||
|
"github.com/theupdateframework/notary/tuf/signed" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
// ReleasesRole is the role named "releases"
|
||||||
|
ReleasesRole = data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "releases")) |
||||||
|
// ActionsPullOnly defines the actions for read-only interactions with a Notary Repository
|
||||||
|
ActionsPullOnly = []string{"pull"} |
||||||
|
// ActionsPushAndPull defines the actions for read-write interactions with a Notary Repository
|
||||||
|
ActionsPushAndPull = []string{"pull", "push"} |
||||||
|
// NotaryServer is the endpoint serving the Notary trust server
|
||||||
|
NotaryServer = "https://notary.docker.io" |
||||||
|
) |
||||||
|
|
||||||
|
// GetTrustDirectory returns the base trust directory name
|
||||||
|
func GetTrustDirectory() string { |
||||||
|
return filepath.Join(cliconfig.Dir(), "trust") |
||||||
|
} |
||||||
|
|
||||||
|
// certificateDirectory returns the directory containing
|
||||||
|
// TLS certificates for the given server. An error is
|
||||||
|
// returned if there was an error parsing the server string.
|
||||||
|
func certificateDirectory(server string) (string, error) { |
||||||
|
u, err := url.Parse(server) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return filepath.Join(cliconfig.Dir(), "tls", u.Host), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Server returns the base URL for the trust server.
|
||||||
|
func Server(index *registrytypes.IndexInfo) (string, error) { |
||||||
|
if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { |
||||||
|
urlObj, err := url.Parse(s) |
||||||
|
if err != nil || urlObj.Scheme != "https" { |
||||||
|
return "", errors.Errorf("valid https URL required for trust server, got %s", s) |
||||||
|
} |
||||||
|
|
||||||
|
return s, nil |
||||||
|
} |
||||||
|
if index.Official { |
||||||
|
return NotaryServer, nil |
||||||
|
} |
||||||
|
return "https://" + index.Name, nil |
||||||
|
} |
||||||
|
|
||||||
|
type simpleCredentialStore struct { |
||||||
|
auth types.AuthConfig |
||||||
|
} |
||||||
|
|
||||||
|
func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) { |
||||||
|
return scs.auth.Username, scs.auth.Password |
||||||
|
} |
||||||
|
|
||||||
|
func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string { |
||||||
|
return scs.auth.IdentityToken |
||||||
|
} |
||||||
|
|
||||||
|
func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) { |
||||||
|
} |
||||||
|
|
||||||
|
// GetNotaryRepository returns a NotaryRepository which stores all the
|
||||||
|
// information needed to operate on a notary repository.
|
||||||
|
// It creates an HTTP transport providing authentication support.
|
||||||
|
func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo *registry.RepositoryInfo, authConfig *types.AuthConfig, actions ...string) (client.Repository, error) { |
||||||
|
server, err := Server(repoInfo.Index) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
var cfg = tlsconfig.ClientDefault() |
||||||
|
cfg.InsecureSkipVerify = !repoInfo.Index.Secure |
||||||
|
|
||||||
|
// Get certificate base directory
|
||||||
|
certDir, err := certificateDirectory(server) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
logrus.Debugf("reading certificate directory: %s", certDir) |
||||||
|
|
||||||
|
if err := registry.ReadCertsDirectory(cfg, certDir); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
base := &http.Transport{ |
||||||
|
Proxy: http.ProxyFromEnvironment, |
||||||
|
Dial: (&net.Dialer{ |
||||||
|
Timeout: 30 * time.Second, |
||||||
|
KeepAlive: 30 * time.Second, |
||||||
|
DualStack: true, |
||||||
|
}).Dial, |
||||||
|
TLSHandshakeTimeout: 10 * time.Second, |
||||||
|
TLSClientConfig: cfg, |
||||||
|
DisableKeepAlives: true, |
||||||
|
} |
||||||
|
|
||||||
|
// Skip configuration headers since request is not going to Docker daemon
|
||||||
|
modifiers := registry.Headers(userAgent, http.Header{}) |
||||||
|
authTransport := transport.NewTransport(base, modifiers...) |
||||||
|
pingClient := &http.Client{ |
||||||
|
Transport: authTransport, |
||||||
|
Timeout: 5 * time.Second, |
||||||
|
} |
||||||
|
endpointStr := server + "/v2/" |
||||||
|
req, err := http.NewRequest("GET", endpointStr, nil) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
challengeManager := challenge.NewSimpleManager() |
||||||
|
|
||||||
|
resp, err := pingClient.Do(req) |
||||||
|
if err != nil { |
||||||
|
// Ignore error on ping to operate in offline mode
|
||||||
|
logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) |
||||||
|
} else { |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
// Add response to the challenge manager to parse out
|
||||||
|
// authentication header and register authentication method
|
||||||
|
if err := challengeManager.AddResponse(resp); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
scope := auth.RepositoryScope{ |
||||||
|
Repository: repoInfo.Name.Name(), |
||||||
|
Actions: actions, |
||||||
|
Class: repoInfo.Class, |
||||||
|
} |
||||||
|
creds := simpleCredentialStore{auth: *authConfig} |
||||||
|
tokenHandlerOptions := auth.TokenHandlerOptions{ |
||||||
|
Transport: authTransport, |
||||||
|
Credentials: creds, |
||||||
|
Scopes: []auth.Scope{scope}, |
||||||
|
ClientID: registry.AuthClientID, |
||||||
|
} |
||||||
|
tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions) |
||||||
|
basicHandler := auth.NewBasicHandler(creds) |
||||||
|
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) |
||||||
|
tr := transport.NewTransport(base, modifiers...) |
||||||
|
|
||||||
|
return client.NewFileCachedRepository( |
||||||
|
GetTrustDirectory(), |
||||||
|
data.GUN(repoInfo.Name.Name()), |
||||||
|
server, |
||||||
|
tr, |
||||||
|
GetPassphraseRetriever(in, out), |
||||||
|
trustpinning.TrustPinConfig{}) |
||||||
|
} |
||||||
|
|
||||||
|
// GetPassphraseRetriever returns a passphrase retriever that utilizes Content Trust env vars
|
||||||
|
func GetPassphraseRetriever(in io.Reader, out io.Writer) notary.PassRetriever { |
||||||
|
aliasMap := map[string]string{ |
||||||
|
"root": "root", |
||||||
|
"snapshot": "repository", |
||||||
|
"targets": "repository", |
||||||
|
"default": "repository", |
||||||
|
} |
||||||
|
baseRetriever := passphrase.PromptRetrieverWithInOut(in, out, aliasMap) |
||||||
|
env := map[string]string{ |
||||||
|
"root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), |
||||||
|
"snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), |
||||||
|
"targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), |
||||||
|
"default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), |
||||||
|
} |
||||||
|
|
||||||
|
return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { |
||||||
|
if v := env[alias]; v != "" { |
||||||
|
return v, numAttempts > 1, nil |
||||||
|
} |
||||||
|
// For non-root roles, we can also try the "default" alias if it is specified
|
||||||
|
if v := env["default"]; v != "" && alias != data.CanonicalRootRole.String() { |
||||||
|
return v, numAttempts > 1, nil |
||||||
|
} |
||||||
|
return baseRetriever(keyName, alias, createNew, numAttempts) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// NotaryError formats an error message received from the notary service
|
||||||
|
func NotaryError(repoName string, err error) error { |
||||||
|
switch err.(type) { |
||||||
|
case *json.SyntaxError: |
||||||
|
logrus.Debugf("Notary syntax error: %s", err) |
||||||
|
return errors.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName) |
||||||
|
case signed.ErrExpired: |
||||||
|
return errors.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) |
||||||
|
case trustmanager.ErrKeyNotFound: |
||||||
|
return errors.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) |
||||||
|
case storage.NetworkError: |
||||||
|
return errors.Errorf("Error: error contacting notary server: %v", err) |
||||||
|
case storage.ErrMetaNotFound: |
||||||
|
return errors.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) |
||||||
|
case trustpinning.ErrRootRotationFail, trustpinning.ErrValidationFail, signed.ErrInvalidKeyType: |
||||||
|
return errors.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) |
||||||
|
case signed.ErrNoKeys: |
||||||
|
return errors.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) |
||||||
|
case signed.ErrLowVersion: |
||||||
|
return errors.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) |
||||||
|
case signed.ErrRoleThreshold: |
||||||
|
return errors.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) |
||||||
|
case client.ErrRepositoryNotExist: |
||||||
|
return errors.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) |
||||||
|
case signed.ErrInsufficientSignatures: |
||||||
|
return errors.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) |
||||||
|
} |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// GetSignableRoles returns a list of roles for which we have valid signing
|
||||||
|
// keys, given a notary repository and a target
|
||||||
|
func GetSignableRoles(repo client.Repository, target *client.Target) ([]data.RoleName, error) { |
||||||
|
var signableRoles []data.RoleName |
||||||
|
|
||||||
|
// translate the full key names, which includes the GUN, into just the key IDs
|
||||||
|
allCanonicalKeyIDs := make(map[string]struct{}) |
||||||
|
for fullKeyID := range repo.GetCryptoService().ListAllKeys() { |
||||||
|
allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{} |
||||||
|
} |
||||||
|
|
||||||
|
allDelegationRoles, err := repo.GetDelegationRoles() |
||||||
|
if err != nil { |
||||||
|
return signableRoles, err |
||||||
|
} |
||||||
|
|
||||||
|
// if there are no delegation roles, then just try to sign it into the targets role
|
||||||
|
if len(allDelegationRoles) == 0 { |
||||||
|
signableRoles = append(signableRoles, data.CanonicalTargetsRole) |
||||||
|
return signableRoles, nil |
||||||
|
} |
||||||
|
|
||||||
|
// there are delegation roles, find every delegation role we have a key for, and
|
||||||
|
// attempt to sign into into all those roles.
|
||||||
|
for _, delegationRole := range allDelegationRoles { |
||||||
|
// We do not support signing any delegation role that isn't a direct child of the targets role.
|
||||||
|
// Also don't bother checking the keys if we can't add the target
|
||||||
|
// to this role due to path restrictions
|
||||||
|
if path.Dir(delegationRole.Name.String()) != data.CanonicalTargetsRole.String() || !delegationRole.CheckPaths(target.Name) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
for _, canonicalKeyID := range delegationRole.KeyIDs { |
||||||
|
if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok { |
||||||
|
signableRoles = append(signableRoles, delegationRole.Name) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(signableRoles) == 0 { |
||||||
|
return signableRoles, errors.Errorf("no valid signing keys for delegation roles") |
||||||
|
} |
||||||
|
|
||||||
|
return signableRoles, nil |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// ImageRefAndAuth contains all reference information and the auth config for an image request
|
||||||
|
type ImageRefAndAuth struct { |
||||||
|
original string |
||||||
|
authConfig *types.AuthConfig |
||||||
|
reference reference.Named |
||||||
|
repoInfo *registry.RepositoryInfo |
||||||
|
tag string |
||||||
|
digest digest.Digest |
||||||
|
} |
||||||
|
|
||||||
|
// GetImageReferencesAndAuth retrieves the necessary reference and auth information for an image name
|
||||||
|
// as an ImageRefAndAuth struct
|
||||||
|
func GetImageReferencesAndAuth(ctx context.Context, rs registry.Service, |
||||||
|
authResolver func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig, |
||||||
|
imgName string, |
||||||
|
) (ImageRefAndAuth, error) { |
||||||
|
ref, err := reference.ParseNormalizedNamed(imgName) |
||||||
|
if err != nil { |
||||||
|
return ImageRefAndAuth{}, err |
||||||
|
} |
||||||
|
|
||||||
|
// Resolve the Repository name from fqn to RepositoryInfo
|
||||||
|
var repoInfo *registry.RepositoryInfo |
||||||
|
if rs != nil { |
||||||
|
repoInfo, err = rs.ResolveRepository(ref) |
||||||
|
} else { |
||||||
|
repoInfo, err = registry.ParseRepositoryInfo(ref) |
||||||
|
} |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return ImageRefAndAuth{}, err |
||||||
|
} |
||||||
|
|
||||||
|
authConfig := authResolver(ctx, repoInfo.Index) |
||||||
|
return ImageRefAndAuth{ |
||||||
|
original: imgName, |
||||||
|
authConfig: &authConfig, |
||||||
|
reference: ref, |
||||||
|
repoInfo: repoInfo, |
||||||
|
tag: getTag(ref), |
||||||
|
digest: getDigest(ref), |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func getTag(ref reference.Named) string { |
||||||
|
switch x := ref.(type) { |
||||||
|
case reference.Canonical, reference.Digested: |
||||||
|
return "" |
||||||
|
case reference.NamedTagged: |
||||||
|
return x.Tag() |
||||||
|
default: |
||||||
|
return "" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func getDigest(ref reference.Named) digest.Digest { |
||||||
|
switch x := ref.(type) { |
||||||
|
case reference.Canonical: |
||||||
|
return x.Digest() |
||||||
|
case reference.Digested: |
||||||
|
return x.Digest() |
||||||
|
default: |
||||||
|
return digest.Digest("") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// AuthConfig returns the auth information (username, etc) for a given ImageRefAndAuth
|
||||||
|
func (imgRefAuth *ImageRefAndAuth) AuthConfig() *types.AuthConfig { |
||||||
|
return imgRefAuth.authConfig |
||||||
|
} |
||||||
|
|
||||||
|
// Reference returns the Image reference for a given ImageRefAndAuth
|
||||||
|
func (imgRefAuth *ImageRefAndAuth) Reference() reference.Named { |
||||||
|
return imgRefAuth.reference |
||||||
|
} |
||||||
|
|
||||||
|
// RepoInfo returns the repository information for a given ImageRefAndAuth
|
||||||
|
func (imgRefAuth *ImageRefAndAuth) RepoInfo() *registry.RepositoryInfo { |
||||||
|
return imgRefAuth.repoInfo |
||||||
|
} |
||||||
|
|
||||||
|
// Tag returns the Image tag for a given ImageRefAndAuth
|
||||||
|
func (imgRefAuth *ImageRefAndAuth) Tag() string { |
||||||
|
return imgRefAuth.tag |
||||||
|
} |
||||||
|
|
||||||
|
// Digest returns the Image digest for a given ImageRefAndAuth
|
||||||
|
func (imgRefAuth *ImageRefAndAuth) Digest() digest.Digest { |
||||||
|
return imgRefAuth.digest |
||||||
|
} |
||||||
|
|
||||||
|
// Name returns the image name used to initialize the ImageRefAndAuth
|
||||||
|
func (imgRefAuth *ImageRefAndAuth) Name() string { |
||||||
|
return imgRefAuth.original |
||||||
|
|
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
package version |
||||||
|
|
||||||
|
// Default build-time variable.
|
||||||
|
// These values are overridden via ldflags
|
||||||
|
var ( |
||||||
|
PlatformName = "" |
||||||
|
Version = "unknown-version" |
||||||
|
GitCommit = "unknown-commit" |
||||||
|
BuildTime = "unknown-buildtime" |
||||||
|
) |
@ -0,0 +1,216 @@ |
|||||||
|
package manifestlist |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/docker/distribution" |
||||||
|
"github.com/docker/distribution/manifest" |
||||||
|
"github.com/opencontainers/go-digest" |
||||||
|
"github.com/opencontainers/image-spec/specs-go/v1" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
// MediaTypeManifestList specifies the mediaType for manifest lists.
|
||||||
|
MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" |
||||||
|
) |
||||||
|
|
||||||
|
// SchemaVersion provides a pre-initialized version structure for this
|
||||||
|
// packages version of the manifest.
|
||||||
|
var SchemaVersion = manifest.Versioned{ |
||||||
|
SchemaVersion: 2, |
||||||
|
MediaType: MediaTypeManifestList, |
||||||
|
} |
||||||
|
|
||||||
|
// OCISchemaVersion provides a pre-initialized version structure for this
|
||||||
|
// packages OCIschema version of the manifest.
|
||||||
|
var OCISchemaVersion = manifest.Versioned{ |
||||||
|
SchemaVersion: 2, |
||||||
|
MediaType: v1.MediaTypeImageIndex, |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { |
||||||
|
m := new(DeserializedManifestList) |
||||||
|
err := m.UnmarshalJSON(b) |
||||||
|
if err != nil { |
||||||
|
return nil, distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
|
||||||
|
if m.MediaType != MediaTypeManifestList { |
||||||
|
err = fmt.Errorf("mediaType in manifest list should be '%s' not '%s'", |
||||||
|
MediaTypeManifestList, m.MediaType) |
||||||
|
|
||||||
|
return nil, distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
|
||||||
|
dgst := digest.FromBytes(b) |
||||||
|
return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifestList}, err |
||||||
|
} |
||||||
|
err := distribution.RegisterManifestSchema(MediaTypeManifestList, manifestListFunc) |
||||||
|
if err != nil { |
||||||
|
panic(fmt.Sprintf("Unable to register manifest: %s", err)) |
||||||
|
} |
||||||
|
|
||||||
|
imageIndexFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { |
||||||
|
m := new(DeserializedManifestList) |
||||||
|
err := m.UnmarshalJSON(b) |
||||||
|
if err != nil { |
||||||
|
return nil, distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
|
||||||
|
if m.MediaType != "" && m.MediaType != v1.MediaTypeImageIndex { |
||||||
|
err = fmt.Errorf("if present, mediaType in image index should be '%s' not '%s'", |
||||||
|
v1.MediaTypeImageIndex, m.MediaType) |
||||||
|
|
||||||
|
return nil, distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
|
||||||
|
dgst := digest.FromBytes(b) |
||||||
|
return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: v1.MediaTypeImageIndex}, err |
||||||
|
} |
||||||
|
err = distribution.RegisterManifestSchema(v1.MediaTypeImageIndex, imageIndexFunc) |
||||||
|
if err != nil { |
||||||
|
panic(fmt.Sprintf("Unable to register OCI Image Index: %s", err)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// PlatformSpec specifies a platform where a particular image manifest is
|
||||||
|
// applicable.
|
||||||
|
type PlatformSpec struct { |
||||||
|
// Architecture field specifies the CPU architecture, for example
|
||||||
|
// `amd64` or `ppc64`.
|
||||||
|
Architecture string `json:"architecture"` |
||||||
|
|
||||||
|
// OS specifies the operating system, for example `linux` or `windows`.
|
||||||
|
OS string `json:"os"` |
||||||
|
|
||||||
|
// OSVersion is an optional field specifying the operating system
|
||||||
|
// version, for example `10.0.10586`.
|
||||||
|
OSVersion string `json:"os.version,omitempty"` |
||||||
|
|
||||||
|
// OSFeatures is an optional field specifying an array of strings,
|
||||||
|
// each listing a required OS feature (for example on Windows `win32k`).
|
||||||
|
OSFeatures []string `json:"os.features,omitempty"` |
||||||
|
|
||||||
|
// Variant is an optional field specifying a variant of the CPU, for
|
||||||
|
// example `ppc64le` to specify a little-endian version of a PowerPC CPU.
|
||||||
|
Variant string `json:"variant,omitempty"` |
||||||
|
|
||||||
|
// Features is an optional field specifying an array of strings, each
|
||||||
|
// listing a required CPU feature (for example `sse4` or `aes`).
|
||||||
|
Features []string `json:"features,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
// A ManifestDescriptor references a platform-specific manifest.
|
||||||
|
type ManifestDescriptor struct { |
||||||
|
distribution.Descriptor |
||||||
|
|
||||||
|
// Platform specifies which platform the manifest pointed to by the
|
||||||
|
// descriptor runs on.
|
||||||
|
Platform PlatformSpec `json:"platform"` |
||||||
|
} |
||||||
|
|
||||||
|
// ManifestList references manifests for various platforms.
|
||||||
|
type ManifestList struct { |
||||||
|
manifest.Versioned |
||||||
|
|
||||||
|
// Config references the image configuration as a blob.
|
||||||
|
Manifests []ManifestDescriptor `json:"manifests"` |
||||||
|
} |
||||||
|
|
||||||
|
// References returns the distribution descriptors for the referenced image
|
||||||
|
// manifests.
|
||||||
|
func (m ManifestList) References() []distribution.Descriptor { |
||||||
|
dependencies := make([]distribution.Descriptor, len(m.Manifests)) |
||||||
|
for i := range m.Manifests { |
||||||
|
dependencies[i] = m.Manifests[i].Descriptor |
||||||
|
} |
||||||
|
|
||||||
|
return dependencies |
||||||
|
} |
||||||
|
|
||||||
|
// DeserializedManifestList wraps ManifestList with a copy of the original
|
||||||
|
// JSON.
|
||||||
|
type DeserializedManifestList struct { |
||||||
|
ManifestList |
||||||
|
|
||||||
|
// canonical is the canonical byte representation of the Manifest.
|
||||||
|
canonical []byte |
||||||
|
} |
||||||
|
|
||||||
|
// FromDescriptors takes a slice of descriptors, and returns a
|
||||||
|
// DeserializedManifestList which contains the resulting manifest list
|
||||||
|
// and its JSON representation.
|
||||||
|
func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) { |
||||||
|
var mediaType string |
||||||
|
if len(descriptors) > 0 && descriptors[0].Descriptor.MediaType == v1.MediaTypeImageManifest { |
||||||
|
mediaType = v1.MediaTypeImageIndex |
||||||
|
} else { |
||||||
|
mediaType = MediaTypeManifestList |
||||||
|
} |
||||||
|
|
||||||
|
return FromDescriptorsWithMediaType(descriptors, mediaType) |
||||||
|
} |
||||||
|
|
||||||
|
// FromDescriptorsWithMediaType is for testing purposes, it's useful to be able to specify the media type explicitly
|
||||||
|
func FromDescriptorsWithMediaType(descriptors []ManifestDescriptor, mediaType string) (*DeserializedManifestList, error) { |
||||||
|
m := ManifestList{ |
||||||
|
Versioned: manifest.Versioned{ |
||||||
|
SchemaVersion: 2, |
||||||
|
MediaType: mediaType, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
m.Manifests = make([]ManifestDescriptor, len(descriptors), len(descriptors)) |
||||||
|
copy(m.Manifests, descriptors) |
||||||
|
|
||||||
|
deserialized := DeserializedManifestList{ |
||||||
|
ManifestList: m, |
||||||
|
} |
||||||
|
|
||||||
|
var err error |
||||||
|
deserialized.canonical, err = json.MarshalIndent(&m, "", " ") |
||||||
|
return &deserialized, err |
||||||
|
} |
||||||
|
|
||||||
|
// UnmarshalJSON populates a new ManifestList struct from JSON data.
|
||||||
|
func (m *DeserializedManifestList) UnmarshalJSON(b []byte) error { |
||||||
|
m.canonical = make([]byte, len(b), len(b)) |
||||||
|
// store manifest list in canonical
|
||||||
|
copy(m.canonical, b) |
||||||
|
|
||||||
|
// Unmarshal canonical JSON into ManifestList object
|
||||||
|
var manifestList ManifestList |
||||||
|
if err := json.Unmarshal(m.canonical, &manifestList); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
m.ManifestList = manifestList |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// MarshalJSON returns the contents of canonical. If canonical is empty,
|
||||||
|
// marshals the inner contents.
|
||||||
|
func (m *DeserializedManifestList) MarshalJSON() ([]byte, error) { |
||||||
|
if len(m.canonical) > 0 { |
||||||
|
return m.canonical, nil |
||||||
|
} |
||||||
|
|
||||||
|
return nil, errors.New("JSON representation not initialized in DeserializedManifestList") |
||||||
|
} |
||||||
|
|
||||||
|
// Payload returns the raw content of the manifest list. The contents can be
|
||||||
|
// used to calculate the content identifier.
|
||||||
|
func (m DeserializedManifestList) Payload() (string, []byte, error) { |
||||||
|
var mediaType string |
||||||
|
if m.MediaType == "" { |
||||||
|
mediaType = v1.MediaTypeImageIndex |
||||||
|
} else { |
||||||
|
mediaType = m.MediaType |
||||||
|
} |
||||||
|
|
||||||
|
return mediaType, m.canonical, nil |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
package metrics |
||||||
|
|
||||||
|
import "github.com/docker/go-metrics" |
||||||
|
|
||||||
|
const ( |
||||||
|
// NamespacePrefix is the namespace of prometheus metrics
|
||||||
|
NamespacePrefix = "registry" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
// StorageNamespace is the prometheus namespace of blob/cache related operations
|
||||||
|
StorageNamespace = metrics.NewNamespace(NamespacePrefix, "storage", nil) |
||||||
|
) |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,9 @@ |
|||||||
|
// Package v2 describes routes, urls and the error codes used in the Docker
|
||||||
|
// Registry JSON HTTP API V2. In addition to declarations, descriptors are
|
||||||
|
// provided for routes and error codes that can be used for implementation and
|
||||||
|
// automatically generating documentation.
|
||||||
|
//
|
||||||
|
// Definitions here are considered to be locked down for the V2 registry api.
|
||||||
|
// Any changes must be considered carefully and should not proceed without a
|
||||||
|
// change proposal in docker core.
|
||||||
|
package v2 |
@ -0,0 +1,136 @@ |
|||||||
|
package v2 |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/docker/distribution/registry/api/errcode" |
||||||
|
) |
||||||
|
|
||||||
|
const errGroup = "registry.api.v2" |
||||||
|
|
||||||
|
var ( |
||||||
|
// ErrorCodeDigestInvalid is returned when uploading a blob if the
|
||||||
|
// provided digest does not match the blob contents.
|
||||||
|
ErrorCodeDigestInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ |
||||||
|
Value: "DIGEST_INVALID", |
||||||
|
Message: "provided digest did not match uploaded content", |
||||||
|
Description: `When a blob is uploaded, the registry will check that |
||||||
|
the content matches the digest provided by the client. The error may |
||||||
|
include a detail structure with the key "digest", including the |
||||||
|
invalid digest string. This error may also be returned when a manifest |
||||||
|
includes an invalid layer digest.`, |
||||||
|
HTTPStatusCode: http.StatusBadRequest, |
||||||
|
}) |
||||||
|
|
||||||
|
// ErrorCodeSizeInvalid is returned when uploading a blob if the provided
|
||||||
|
ErrorCodeSizeInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ |
||||||
|
Value: "SIZE_INVALID", |
||||||
|
Message: "provided length did not match content length", |
||||||
|
Description: `When a layer is uploaded, the provided size will be |
||||||
|
checked against the uploaded content. If they do not match, this error |
||||||
|
will be returned.`, |
||||||
|
HTTPStatusCode: http.StatusBadRequest, |
||||||
|
}) |
||||||
|
|
||||||
|
// ErrorCodeNameInvalid is returned when the name in the manifest does not
|
||||||
|
// match the provided name.
|
||||||
|
ErrorCodeNameInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ |
||||||
|
Value: "NAME_INVALID", |
||||||
|
Message: "invalid repository name", |
||||||
|
Description: `Invalid repository name encountered either during |
||||||
|
manifest validation or any API operation.`, |
||||||
|
HTTPStatusCode: http.StatusBadRequest, |
||||||
|
}) |
||||||
|
|
||||||
|
// ErrorCodeTagInvalid is returned when the tag in the manifest does not
|
||||||
|
// match the provided tag.
|
||||||
|
ErrorCodeTagInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ |
||||||
|
Value: "TAG_INVALID", |
||||||
|
Message: "manifest tag did not match URI", |
||||||
|
Description: `During a manifest upload, if the tag in the manifest |
||||||
|
does not match the uri tag, this error will be returned.`, |
||||||
|
HTTPStatusCode: http.StatusBadRequest, |
||||||
|
}) |
||||||
|
|
||||||
|
// ErrorCodeNameUnknown when the repository name is not known.
|
||||||
|
ErrorCodeNameUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ |
||||||
|
Value: "NAME_UNKNOWN", |
||||||
|
Message: "repository name not known to registry", |
||||||
|
Description: `This is returned if the name used during an operation is |
||||||
|
unknown to the registry.`, |
||||||
|
HTTPStatusCode: http.StatusNotFound, |
||||||
|
}) |
||||||
|
|
||||||
|
// ErrorCodeManifestUnknown returned when image manifest is unknown.
|
||||||
|
ErrorCodeManifestUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ |
||||||
|
Value: "MANIFEST_UNKNOWN", |
||||||
|
Message: "manifest unknown", |
||||||
|
Description: `This error is returned when the manifest, identified by |
||||||
|
name and tag is unknown to the repository.`, |
||||||
|
HTTPStatusCode: http.StatusNotFound, |
||||||
|
}) |
||||||
|
|
||||||
|
// ErrorCodeManifestInvalid returned when an image manifest is invalid,
|
||||||
|
// typically during a PUT operation. This error encompasses all errors
|
||||||
|
// encountered during manifest validation that aren't signature errors.
|
||||||
|
ErrorCodeManifestInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ |
||||||
|
Value: "MANIFEST_INVALID", |
||||||
|
Message: "manifest invalid", |
||||||
|
Description: `During upload, manifests undergo several checks ensuring |
||||||
|
validity. If those checks fail, this error may be returned, unless a |
||||||
|
more specific error is included. The detail will contain information |
||||||
|
the failed validation.`, |
||||||
|
HTTPStatusCode: http.StatusBadRequest, |
||||||
|
}) |
||||||
|
|
||||||
|
// ErrorCodeManifestUnverified is returned when the manifest fails
|
||||||
|
// signature verification.
|
||||||
|
ErrorCodeManifestUnverified = errcode.Register(errGroup, errcode.ErrorDescriptor{ |
||||||
|
Value: "MANIFEST_UNVERIFIED", |
||||||
|
Message: "manifest failed signature verification", |
||||||
|
Description: `During manifest upload, if the manifest fails signature |
||||||
|
verification, this error will be returned.`, |
||||||
|
HTTPStatusCode: http.StatusBadRequest, |
||||||
|
}) |
||||||
|
|
||||||
|
// ErrorCodeManifestBlobUnknown is returned when a manifest blob is
|
||||||
|
// unknown to the registry.
|
||||||
|
ErrorCodeManifestBlobUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ |
||||||
|
Value: "MANIFEST_BLOB_UNKNOWN", |
||||||
|
Message: "blob unknown to registry", |
||||||
|
Description: `This error may be returned when a manifest blob is
|
||||||
|
unknown to the registry.`, |
||||||
|
HTTPStatusCode: http.StatusBadRequest, |
||||||
|
}) |
||||||
|
|
||||||
|
// ErrorCodeBlobUnknown is returned when a blob is unknown to the
|
||||||
|
// registry. This can happen when the manifest references a nonexistent
|
||||||
|
// layer or the result is not found by a blob fetch.
|
||||||
|
ErrorCodeBlobUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ |
||||||
|
Value: "BLOB_UNKNOWN", |
||||||
|
Message: "blob unknown to registry", |
||||||
|
Description: `This error may be returned when a blob is unknown to the |
||||||
|
registry in a specified repository. This can be returned with a |
||||||
|
standard get or if a manifest references an unknown layer during |
||||||
|
upload.`, |
||||||
|
HTTPStatusCode: http.StatusNotFound, |
||||||
|
}) |
||||||
|
|
||||||
|
// ErrorCodeBlobUploadUnknown is returned when an upload is unknown.
|
||||||
|
ErrorCodeBlobUploadUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ |
||||||
|
Value: "BLOB_UPLOAD_UNKNOWN", |
||||||
|
Message: "blob upload unknown to registry", |
||||||
|
Description: `If a blob upload has been cancelled or was never |
||||||
|
started, this error code may be returned.`, |
||||||
|
HTTPStatusCode: http.StatusNotFound, |
||||||
|
}) |
||||||
|
|
||||||
|
// ErrorCodeBlobUploadInvalid is returned when an upload is invalid.
|
||||||
|
ErrorCodeBlobUploadInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ |
||||||
|
Value: "BLOB_UPLOAD_INVALID", |
||||||
|
Message: "blob upload invalid", |
||||||
|
Description: `The blob upload encountered an error and can no |
||||||
|
longer proceed.`, |
||||||
|
HTTPStatusCode: http.StatusNotFound, |
||||||
|
}) |
||||||
|
) |
@ -0,0 +1,161 @@ |
|||||||
|
package v2 |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
"unicode" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
// according to rfc7230
|
||||||
|
reToken = regexp.MustCompile(`^[^"(),/:;<=>?@[\]{}[:space:][:cntrl:]]+`) |
||||||
|
reQuotedValue = regexp.MustCompile(`^[^\\"]+`) |
||||||
|
reEscapedCharacter = regexp.MustCompile(`^[[:blank:][:graph:]]`) |
||||||
|
) |
||||||
|
|
||||||
|
// parseForwardedHeader is a benevolent parser of Forwarded header defined in rfc7239. The header contains
|
||||||
|
// a comma-separated list of forwarding key-value pairs. Each list element is set by single proxy. The
|
||||||
|
// function parses only the first element of the list, which is set by the very first proxy. It returns a map
|
||||||
|
// of corresponding key-value pairs and an unparsed slice of the input string.
|
||||||
|
//
|
||||||
|
// Examples of Forwarded header values:
|
||||||
|
//
|
||||||
|
// 1. Forwarded: For=192.0.2.43; Proto=https,For="[2001:db8:cafe::17]",For=unknown
|
||||||
|
// 2. Forwarded: for="192.0.2.43:443"; host="registry.example.org", for="10.10.05.40:80"
|
||||||
|
//
|
||||||
|
// The first will be parsed into {"for": "192.0.2.43", "proto": "https"} while the second into
|
||||||
|
// {"for": "192.0.2.43:443", "host": "registry.example.org"}.
|
||||||
|
func parseForwardedHeader(forwarded string) (map[string]string, string, error) { |
||||||
|
// Following are states of forwarded header parser. Any state could transition to a failure.
|
||||||
|
const ( |
||||||
|
// terminating state; can transition to Parameter
|
||||||
|
stateElement = iota |
||||||
|
// terminating state; can transition to KeyValueDelimiter
|
||||||
|
stateParameter |
||||||
|
// can transition to Value
|
||||||
|
stateKeyValueDelimiter |
||||||
|
// can transition to one of { QuotedValue, PairEnd }
|
||||||
|
stateValue |
||||||
|
// can transition to one of { EscapedCharacter, PairEnd }
|
||||||
|
stateQuotedValue |
||||||
|
// can transition to one of { QuotedValue }
|
||||||
|
stateEscapedCharacter |
||||||
|
// terminating state; can transition to one of { Parameter, Element }
|
||||||
|
statePairEnd |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
parameter string |
||||||
|
value string |
||||||
|
parse = forwarded[:] |
||||||
|
res = map[string]string{} |
||||||
|
state = stateElement |
||||||
|
) |
||||||
|
|
||||||
|
Loop: |
||||||
|
for { |
||||||
|
// skip spaces unless in quoted value
|
||||||
|
if state != stateQuotedValue && state != stateEscapedCharacter { |
||||||
|
parse = strings.TrimLeftFunc(parse, unicode.IsSpace) |
||||||
|
} |
||||||
|
|
||||||
|
if len(parse) == 0 { |
||||||
|
if state != stateElement && state != statePairEnd && state != stateParameter { |
||||||
|
return nil, parse, fmt.Errorf("unexpected end of input") |
||||||
|
} |
||||||
|
// terminating
|
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
switch state { |
||||||
|
// terminate at list element delimiter
|
||||||
|
case stateElement: |
||||||
|
if parse[0] == ',' { |
||||||
|
parse = parse[1:] |
||||||
|
break Loop |
||||||
|
} |
||||||
|
state = stateParameter |
||||||
|
|
||||||
|
// parse parameter (the key of key-value pair)
|
||||||
|
case stateParameter: |
||||||
|
match := reToken.FindString(parse) |
||||||
|
if len(match) == 0 { |
||||||
|
return nil, parse, fmt.Errorf("failed to parse token at position %d", len(forwarded)-len(parse)) |
||||||
|
} |
||||||
|
parameter = strings.ToLower(match) |
||||||
|
parse = parse[len(match):] |
||||||
|
state = stateKeyValueDelimiter |
||||||
|
|
||||||
|
// parse '='
|
||||||
|
case stateKeyValueDelimiter: |
||||||
|
if parse[0] != '=' { |
||||||
|
return nil, parse, fmt.Errorf("expected '=', not '%c' at position %d", parse[0], len(forwarded)-len(parse)) |
||||||
|
} |
||||||
|
parse = parse[1:] |
||||||
|
state = stateValue |
||||||
|
|
||||||
|
// parse value or quoted value
|
||||||
|
case stateValue: |
||||||
|
if parse[0] == '"' { |
||||||
|
parse = parse[1:] |
||||||
|
state = stateQuotedValue |
||||||
|
} else { |
||||||
|
value = reToken.FindString(parse) |
||||||
|
if len(value) == 0 { |
||||||
|
return nil, parse, fmt.Errorf("failed to parse value at position %d", len(forwarded)-len(parse)) |
||||||
|
} |
||||||
|
if _, exists := res[parameter]; exists { |
||||||
|
return nil, parse, fmt.Errorf("duplicate parameter %q at position %d", parameter, len(forwarded)-len(parse)) |
||||||
|
} |
||||||
|
res[parameter] = value |
||||||
|
parse = parse[len(value):] |
||||||
|
value = "" |
||||||
|
state = statePairEnd |
||||||
|
} |
||||||
|
|
||||||
|
// parse a part of quoted value until the first backslash
|
||||||
|
case stateQuotedValue: |
||||||
|
match := reQuotedValue.FindString(parse) |
||||||
|
value += match |
||||||
|
parse = parse[len(match):] |
||||||
|
switch { |
||||||
|
case len(parse) == 0: |
||||||
|
return nil, parse, fmt.Errorf("unterminated quoted string") |
||||||
|
case parse[0] == '"': |
||||||
|
res[parameter] = value |
||||||
|
value = "" |
||||||
|
parse = parse[1:] |
||||||
|
state = statePairEnd |
||||||
|
case parse[0] == '\\': |
||||||
|
parse = parse[1:] |
||||||
|
state = stateEscapedCharacter |
||||||
|
} |
||||||
|
|
||||||
|
// parse escaped character in a quoted string, ignore the backslash
|
||||||
|
// transition back to QuotedValue state
|
||||||
|
case stateEscapedCharacter: |
||||||
|
c := reEscapedCharacter.FindString(parse) |
||||||
|
if len(c) == 0 { |
||||||
|
return nil, parse, fmt.Errorf("invalid escape sequence at position %d", len(forwarded)-len(parse)-1) |
||||||
|
} |
||||||
|
value += c |
||||||
|
parse = parse[1:] |
||||||
|
state = stateQuotedValue |
||||||
|
|
||||||
|
// expect either a new key-value pair, new list or end of input
|
||||||
|
case statePairEnd: |
||||||
|
switch parse[0] { |
||||||
|
case ';': |
||||||
|
parse = parse[1:] |
||||||
|
state = stateParameter |
||||||
|
case ',': |
||||||
|
state = stateElement |
||||||
|
default: |
||||||
|
return nil, parse, fmt.Errorf("expected ',' or ';', not %c at position %d", parse[0], len(forwarded)-len(parse)) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return res, parse, nil |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
package v2 |
||||||
|
|
||||||
|
import "github.com/gorilla/mux" |
||||||
|
|
||||||
|
// The following are definitions of the name under which all V2 routes are
|
||||||
|
// registered. These symbols can be used to look up a route based on the name.
|
||||||
|
const ( |
||||||
|
RouteNameBase = "base" |
||||||
|
RouteNameManifest = "manifest" |
||||||
|
RouteNameTags = "tags" |
||||||
|
RouteNameBlob = "blob" |
||||||
|
RouteNameBlobUpload = "blob-upload" |
||||||
|
RouteNameBlobUploadChunk = "blob-upload-chunk" |
||||||
|
RouteNameCatalog = "catalog" |
||||||
|
) |
||||||
|
|
||||||
|
// Router builds a gorilla router with named routes for the various API
|
||||||
|
// methods. This can be used directly by both server implementations and
|
||||||
|
// clients.
|
||||||
|
func Router() *mux.Router { |
||||||
|
return RouterWithPrefix("") |
||||||
|
} |
||||||
|
|
||||||
|
// RouterWithPrefix builds a gorilla router with a configured prefix
|
||||||
|
// on all routes.
|
||||||
|
func RouterWithPrefix(prefix string) *mux.Router { |
||||||
|
rootRouter := mux.NewRouter() |
||||||
|
router := rootRouter |
||||||
|
if prefix != "" { |
||||||
|
router = router.PathPrefix(prefix).Subrouter() |
||||||
|
} |
||||||
|
|
||||||
|
router.StrictSlash(true) |
||||||
|
|
||||||
|
for _, descriptor := range routeDescriptors { |
||||||
|
router.Path(descriptor.Path).Name(descriptor.Name) |
||||||
|
} |
||||||
|
|
||||||
|
return rootRouter |
||||||
|
} |
@ -0,0 +1,266 @@ |
|||||||
|
package v2 |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/docker/distribution/reference" |
||||||
|
"github.com/gorilla/mux" |
||||||
|
) |
||||||
|
|
||||||
|
// URLBuilder creates registry API urls from a single base endpoint. It can be
|
||||||
|
// used to create urls for use in a registry client or server.
|
||||||
|
//
|
||||||
|
// All urls will be created from the given base, including the api version.
|
||||||
|
// For example, if a root of "/foo/" is provided, urls generated will be fall
|
||||||
|
// under "/foo/v2/...". Most application will only provide a schema, host and
|
||||||
|
// port, such as "https://localhost:5000/".
|
||||||
|
type URLBuilder struct { |
||||||
|
root *url.URL // url root (ie http://localhost/)
|
||||||
|
router *mux.Router |
||||||
|
relative bool |
||||||
|
} |
||||||
|
|
||||||
|
// NewURLBuilder creates a URLBuilder with provided root url object.
|
||||||
|
func NewURLBuilder(root *url.URL, relative bool) *URLBuilder { |
||||||
|
return &URLBuilder{ |
||||||
|
root: root, |
||||||
|
router: Router(), |
||||||
|
relative: relative, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// NewURLBuilderFromString workes identically to NewURLBuilder except it takes
|
||||||
|
// a string argument for the root, returning an error if it is not a valid
|
||||||
|
// url.
|
||||||
|
func NewURLBuilderFromString(root string, relative bool) (*URLBuilder, error) { |
||||||
|
u, err := url.Parse(root) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return NewURLBuilder(u, relative), nil |
||||||
|
} |
||||||
|
|
||||||
|
// NewURLBuilderFromRequest uses information from an *http.Request to
|
||||||
|
// construct the root url.
|
||||||
|
func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder { |
||||||
|
var ( |
||||||
|
scheme = "http" |
||||||
|
host = r.Host |
||||||
|
) |
||||||
|
|
||||||
|
if r.TLS != nil { |
||||||
|
scheme = "https" |
||||||
|
} else if len(r.URL.Scheme) > 0 { |
||||||
|
scheme = r.URL.Scheme |
||||||
|
} |
||||||
|
|
||||||
|
// Handle fowarded headers
|
||||||
|
// Prefer "Forwarded" header as defined by rfc7239 if given
|
||||||
|
// see https://tools.ietf.org/html/rfc7239
|
||||||
|
if forwarded := r.Header.Get("Forwarded"); len(forwarded) > 0 { |
||||||
|
forwardedHeader, _, err := parseForwardedHeader(forwarded) |
||||||
|
if err == nil { |
||||||
|
if fproto := forwardedHeader["proto"]; len(fproto) > 0 { |
||||||
|
scheme = fproto |
||||||
|
} |
||||||
|
if fhost := forwardedHeader["host"]; len(fhost) > 0 { |
||||||
|
host = fhost |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
if forwardedProto := r.Header.Get("X-Forwarded-Proto"); len(forwardedProto) > 0 { |
||||||
|
scheme = forwardedProto |
||||||
|
} |
||||||
|
if forwardedHost := r.Header.Get("X-Forwarded-Host"); len(forwardedHost) > 0 { |
||||||
|
// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
|
||||||
|
// comma-separated list of hosts, to which each proxy appends the
|
||||||
|
// requested host. We want to grab the first from this comma-separated
|
||||||
|
// list.
|
||||||
|
hosts := strings.SplitN(forwardedHost, ",", 2) |
||||||
|
host = strings.TrimSpace(hosts[0]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
basePath := routeDescriptorsMap[RouteNameBase].Path |
||||||
|
|
||||||
|
requestPath := r.URL.Path |
||||||
|
index := strings.Index(requestPath, basePath) |
||||||
|
|
||||||
|
u := &url.URL{ |
||||||
|
Scheme: scheme, |
||||||
|
Host: host, |
||||||
|
} |
||||||
|
|
||||||
|
if index > 0 { |
||||||
|
// N.B. index+1 is important because we want to include the trailing /
|
||||||
|
u.Path = requestPath[0 : index+1] |
||||||
|
} |
||||||
|
|
||||||
|
return NewURLBuilder(u, relative) |
||||||
|
} |
||||||
|
|
||||||
|
// BuildBaseURL constructs a base url for the API, typically just "/v2/".
|
||||||
|
func (ub *URLBuilder) BuildBaseURL() (string, error) { |
||||||
|
route := ub.cloneRoute(RouteNameBase) |
||||||
|
|
||||||
|
baseURL, err := route.URL() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return baseURL.String(), nil |
||||||
|
} |
||||||
|
|
||||||
|
// BuildCatalogURL constructs a url get a catalog of repositories
|
||||||
|
func (ub *URLBuilder) BuildCatalogURL(values ...url.Values) (string, error) { |
||||||
|
route := ub.cloneRoute(RouteNameCatalog) |
||||||
|
|
||||||
|
catalogURL, err := route.URL() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return appendValuesURL(catalogURL, values...).String(), nil |
||||||
|
} |
||||||
|
|
||||||
|
// BuildTagsURL constructs a url to list the tags in the named repository.
|
||||||
|
func (ub *URLBuilder) BuildTagsURL(name reference.Named) (string, error) { |
||||||
|
route := ub.cloneRoute(RouteNameTags) |
||||||
|
|
||||||
|
tagsURL, err := route.URL("name", name.Name()) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return tagsURL.String(), nil |
||||||
|
} |
||||||
|
|
||||||
|
// BuildManifestURL constructs a url for the manifest identified by name and
|
||||||
|
// reference. The argument reference may be either a tag or digest.
|
||||||
|
func (ub *URLBuilder) BuildManifestURL(ref reference.Named) (string, error) { |
||||||
|
route := ub.cloneRoute(RouteNameManifest) |
||||||
|
|
||||||
|
tagOrDigest := "" |
||||||
|
switch v := ref.(type) { |
||||||
|
case reference.Tagged: |
||||||
|
tagOrDigest = v.Tag() |
||||||
|
case reference.Digested: |
||||||
|
tagOrDigest = v.Digest().String() |
||||||
|
default: |
||||||
|
return "", fmt.Errorf("reference must have a tag or digest") |
||||||
|
} |
||||||
|
|
||||||
|
manifestURL, err := route.URL("name", ref.Name(), "reference", tagOrDigest) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return manifestURL.String(), nil |
||||||
|
} |
||||||
|
|
||||||
|
// BuildBlobURL constructs the url for the blob identified by name and dgst.
|
||||||
|
func (ub *URLBuilder) BuildBlobURL(ref reference.Canonical) (string, error) { |
||||||
|
route := ub.cloneRoute(RouteNameBlob) |
||||||
|
|
||||||
|
layerURL, err := route.URL("name", ref.Name(), "digest", ref.Digest().String()) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return layerURL.String(), nil |
||||||
|
} |
||||||
|
|
||||||
|
// BuildBlobUploadURL constructs a url to begin a blob upload in the
|
||||||
|
// repository identified by name.
|
||||||
|
func (ub *URLBuilder) BuildBlobUploadURL(name reference.Named, values ...url.Values) (string, error) { |
||||||
|
route := ub.cloneRoute(RouteNameBlobUpload) |
||||||
|
|
||||||
|
uploadURL, err := route.URL("name", name.Name()) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return appendValuesURL(uploadURL, values...).String(), nil |
||||||
|
} |
||||||
|
|
||||||
|
// BuildBlobUploadChunkURL constructs a url for the upload identified by uuid,
|
||||||
|
// including any url values. This should generally not be used by clients, as
|
||||||
|
// this url is provided by server implementations during the blob upload
|
||||||
|
// process.
|
||||||
|
func (ub *URLBuilder) BuildBlobUploadChunkURL(name reference.Named, uuid string, values ...url.Values) (string, error) { |
||||||
|
route := ub.cloneRoute(RouteNameBlobUploadChunk) |
||||||
|
|
||||||
|
uploadURL, err := route.URL("name", name.Name(), "uuid", uuid) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return appendValuesURL(uploadURL, values...).String(), nil |
||||||
|
} |
||||||
|
|
||||||
|
// clondedRoute returns a clone of the named route from the router. Routes
|
||||||
|
// must be cloned to avoid modifying them during url generation.
|
||||||
|
func (ub *URLBuilder) cloneRoute(name string) clonedRoute { |
||||||
|
route := new(mux.Route) |
||||||
|
root := new(url.URL) |
||||||
|
|
||||||
|
*route = *ub.router.GetRoute(name) // clone the route
|
||||||
|
*root = *ub.root |
||||||
|
|
||||||
|
return clonedRoute{Route: route, root: root, relative: ub.relative} |
||||||
|
} |
||||||
|
|
||||||
|
type clonedRoute struct { |
||||||
|
*mux.Route |
||||||
|
root *url.URL |
||||||
|
relative bool |
||||||
|
} |
||||||
|
|
||||||
|
func (cr clonedRoute) URL(pairs ...string) (*url.URL, error) { |
||||||
|
routeURL, err := cr.Route.URL(pairs...) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if cr.relative { |
||||||
|
return routeURL, nil |
||||||
|
} |
||||||
|
|
||||||
|
if routeURL.Scheme == "" && routeURL.User == nil && routeURL.Host == "" { |
||||||
|
routeURL.Path = routeURL.Path[1:] |
||||||
|
} |
||||||
|
|
||||||
|
url := cr.root.ResolveReference(routeURL) |
||||||
|
url.Scheme = cr.root.Scheme |
||||||
|
return url, nil |
||||||
|
} |
||||||
|
|
||||||
|
// appendValuesURL appends the parameters to the url.
|
||||||
|
func appendValuesURL(u *url.URL, values ...url.Values) *url.URL { |
||||||
|
merged := u.Query() |
||||||
|
|
||||||
|
for _, v := range values { |
||||||
|
for k, vv := range v { |
||||||
|
merged[k] = append(merged[k], vv...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
u.RawQuery = merged.Encode() |
||||||
|
return u |
||||||
|
} |
||||||
|
|
||||||
|
// appendValues appends the parameters to the url. Panics if the string is not
|
||||||
|
// a url.
|
||||||
|
func appendValues(u string, values ...url.Values) string { |
||||||
|
up, err := url.Parse(u) |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
panic(err) // should never happen
|
||||||
|
} |
||||||
|
|
||||||
|
return appendValuesURL(up, values...).String() |
||||||
|
} |
@ -0,0 +1,58 @@ |
|||||||
|
package auth |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// APIVersion represents a version of an API including its
|
||||||
|
// type and version number.
|
||||||
|
type APIVersion struct { |
||||||
|
// Type refers to the name of a specific API specification
|
||||||
|
// such as "registry"
|
||||||
|
Type string |
||||||
|
|
||||||
|
// Version is the version of the API specification implemented,
|
||||||
|
// This may omit the revision number and only include
|
||||||
|
// the major and minor version, such as "2.0"
|
||||||
|
Version string |
||||||
|
} |
||||||
|
|
||||||
|
// String returns the string formatted API Version
|
||||||
|
func (v APIVersion) String() string { |
||||||
|
return v.Type + "/" + v.Version |
||||||
|
} |
||||||
|
|
||||||
|
// APIVersions gets the API versions out of an HTTP response using the provided
|
||||||
|
// version header as the key for the HTTP header.
|
||||||
|
func APIVersions(resp *http.Response, versionHeader string) []APIVersion { |
||||||
|
versions := []APIVersion{} |
||||||
|
if versionHeader != "" { |
||||||
|
for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey(versionHeader)] { |
||||||
|
for _, version := range strings.Fields(supportedVersions) { |
||||||
|
versions = append(versions, ParseAPIVersion(version)) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return versions |
||||||
|
} |
||||||
|
|
||||||
|
// ParseAPIVersion parses an API version string into an APIVersion
|
||||||
|
// Format (Expected, not enforced):
|
||||||
|
// API version string = <API type> '/' <API version>
|
||||||
|
// API type = [a-z][a-z0-9]*
|
||||||
|
// API version = [0-9]+(\.[0-9]+)?
|
||||||
|
// TODO(dmcgowan): Enforce format, add error condition, remove unknown type
|
||||||
|
func ParseAPIVersion(versionStr string) APIVersion { |
||||||
|
idx := strings.IndexRune(versionStr, '/') |
||||||
|
if idx == -1 { |
||||||
|
return APIVersion{ |
||||||
|
Type: "unknown", |
||||||
|
Version: versionStr, |
||||||
|
} |
||||||
|
} |
||||||
|
return APIVersion{ |
||||||
|
Type: strings.ToLower(versionStr[:idx]), |
||||||
|
Version: versionStr[idx+1:], |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
package challenge |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/url" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// FROM: https://golang.org/src/net/http/http.go
|
||||||
|
// Given a string of the form "host", "host:port", or "[ipv6::address]:port",
|
||||||
|
// return true if the string includes a port.
|
||||||
|
func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") } |
||||||
|
|
||||||
|
// FROM: http://golang.org/src/net/http/transport.go
|
||||||
|
var portMap = map[string]string{ |
||||||
|
"http": "80", |
||||||
|
"https": "443", |
||||||
|
} |
||||||
|
|
||||||
|
// canonicalAddr returns url.Host but always with a ":port" suffix
|
||||||
|
// FROM: http://golang.org/src/net/http/transport.go
|
||||||
|
func canonicalAddr(url *url.URL) string { |
||||||
|
addr := url.Host |
||||||
|
if !hasPort(addr) { |
||||||
|
return addr + ":" + portMap[url.Scheme] |
||||||
|
} |
||||||
|
return addr |
||||||
|
} |
237
vendor/github.com/docker/distribution/registry/client/auth/challenge/authchallenge.go
generated
vendored
237
vendor/github.com/docker/distribution/registry/client/auth/challenge/authchallenge.go
generated
vendored
@ -0,0 +1,237 @@ |
|||||||
|
package challenge |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
) |
||||||
|
|
||||||
|
// Challenge carries information from a WWW-Authenticate response header.
|
||||||
|
// See RFC 2617.
|
||||||
|
type Challenge struct { |
||||||
|
// Scheme is the auth-scheme according to RFC 2617
|
||||||
|
Scheme string |
||||||
|
|
||||||
|
// Parameters are the auth-params according to RFC 2617
|
||||||
|
Parameters map[string]string |
||||||
|
} |
||||||
|
|
||||||
|
// Manager manages the challenges for endpoints.
|
||||||
|
// The challenges are pulled out of HTTP responses. Only
|
||||||
|
// responses which expect challenges should be added to
|
||||||
|
// the manager, since a non-unauthorized request will be
|
||||||
|
// viewed as not requiring challenges.
|
||||||
|
type Manager interface { |
||||||
|
// GetChallenges returns the challenges for the given
|
||||||
|
// endpoint URL.
|
||||||
|
GetChallenges(endpoint url.URL) ([]Challenge, error) |
||||||
|
|
||||||
|
// AddResponse adds the response to the challenge
|
||||||
|
// manager. The challenges will be parsed out of
|
||||||
|
// the WWW-Authenicate headers and added to the
|
||||||
|
// URL which was produced the response. If the
|
||||||
|
// response was authorized, any challenges for the
|
||||||
|
// endpoint will be cleared.
|
||||||
|
AddResponse(resp *http.Response) error |
||||||
|
} |
||||||
|
|
||||||
|
// NewSimpleManager returns an instance of
|
||||||
|
// Manger which only maps endpoints to challenges
|
||||||
|
// based on the responses which have been added the
|
||||||
|
// manager. The simple manager will make no attempt to
|
||||||
|
// perform requests on the endpoints or cache the responses
|
||||||
|
// to a backend.
|
||||||
|
func NewSimpleManager() Manager { |
||||||
|
return &simpleManager{ |
||||||
|
Challenges: make(map[string][]Challenge), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type simpleManager struct { |
||||||
|
sync.RWMutex |
||||||
|
Challenges map[string][]Challenge |
||||||
|
} |
||||||
|
|
||||||
|
func normalizeURL(endpoint *url.URL) { |
||||||
|
endpoint.Host = strings.ToLower(endpoint.Host) |
||||||
|
endpoint.Host = canonicalAddr(endpoint) |
||||||
|
} |
||||||
|
|
||||||
|
func (m *simpleManager) GetChallenges(endpoint url.URL) ([]Challenge, error) { |
||||||
|
normalizeURL(&endpoint) |
||||||
|
|
||||||
|
m.RLock() |
||||||
|
defer m.RUnlock() |
||||||
|
challenges := m.Challenges[endpoint.String()] |
||||||
|
return challenges, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (m *simpleManager) AddResponse(resp *http.Response) error { |
||||||
|
challenges := ResponseChallenges(resp) |
||||||
|
if resp.Request == nil { |
||||||
|
return fmt.Errorf("missing request reference") |
||||||
|
} |
||||||
|
urlCopy := url.URL{ |
||||||
|
Path: resp.Request.URL.Path, |
||||||
|
Host: resp.Request.URL.Host, |
||||||
|
Scheme: resp.Request.URL.Scheme, |
||||||
|
} |
||||||
|
normalizeURL(&urlCopy) |
||||||
|
|
||||||
|
m.Lock() |
||||||
|
defer m.Unlock() |
||||||
|
m.Challenges[urlCopy.String()] = challenges |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Octet types from RFC 2616.
|
||||||
|
type octetType byte |
||||||
|
|
||||||
|
var octetTypes [256]octetType |
||||||
|
|
||||||
|
const ( |
||||||
|
isToken octetType = 1 << iota |
||||||
|
isSpace |
||||||
|
) |
||||||
|
|
||||||
|
func init() { |
||||||
|
// OCTET = <any 8-bit sequence of data>
|
||||||
|
// CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||||
|
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
||||||
|
// CR = <US-ASCII CR, carriage return (13)>
|
||||||
|
// LF = <US-ASCII LF, linefeed (10)>
|
||||||
|
// SP = <US-ASCII SP, space (32)>
|
||||||
|
// HT = <US-ASCII HT, horizontal-tab (9)>
|
||||||
|
// <"> = <US-ASCII double-quote mark (34)>
|
||||||
|
// CRLF = CR LF
|
||||||
|
// LWS = [CRLF] 1*( SP | HT )
|
||||||
|
// TEXT = <any OCTET except CTLs, but including LWS>
|
||||||
|
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
|
||||||
|
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
|
||||||
|
// token = 1*<any CHAR except CTLs or separators>
|
||||||
|
// qdtext = <any TEXT except <">>
|
||||||
|
|
||||||
|
for c := 0; c < 256; c++ { |
||||||
|
var t octetType |
||||||
|
isCtl := c <= 31 || c == 127 |
||||||
|
isChar := 0 <= c && c <= 127 |
||||||
|
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0 |
||||||
|
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 { |
||||||
|
t |= isSpace |
||||||
|
} |
||||||
|
if isChar && !isCtl && !isSeparator { |
||||||
|
t |= isToken |
||||||
|
} |
||||||
|
octetTypes[c] = t |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ResponseChallenges returns a list of authorization challenges
|
||||||
|
// for the given http Response. Challenges are only checked if
|
||||||
|
// the response status code was a 401.
|
||||||
|
func ResponseChallenges(resp *http.Response) []Challenge { |
||||||
|
if resp.StatusCode == http.StatusUnauthorized { |
||||||
|
// Parse the WWW-Authenticate Header and store the challenges
|
||||||
|
// on this endpoint object.
|
||||||
|
return parseAuthHeader(resp.Header) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func parseAuthHeader(header http.Header) []Challenge { |
||||||
|
challenges := []Challenge{} |
||||||
|
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] { |
||||||
|
v, p := parseValueAndParams(h) |
||||||
|
if v != "" { |
||||||
|
challenges = append(challenges, Challenge{Scheme: v, Parameters: p}) |
||||||
|
} |
||||||
|
} |
||||||
|
return challenges |
||||||
|
} |
||||||
|
|
||||||
|
func parseValueAndParams(header string) (value string, params map[string]string) { |
||||||
|
params = make(map[string]string) |
||||||
|
value, s := expectToken(header) |
||||||
|
if value == "" { |
||||||
|
return |
||||||
|
} |
||||||
|
value = strings.ToLower(value) |
||||||
|
s = "," + skipSpace(s) |
||||||
|
for strings.HasPrefix(s, ",") { |
||||||
|
var pkey string |
||||||
|
pkey, s = expectToken(skipSpace(s[1:])) |
||||||
|
if pkey == "" { |
||||||
|
return |
||||||
|
} |
||||||
|
if !strings.HasPrefix(s, "=") { |
||||||
|
return |
||||||
|
} |
||||||
|
var pvalue string |
||||||
|
pvalue, s = expectTokenOrQuoted(s[1:]) |
||||||
|
if pvalue == "" { |
||||||
|
return |
||||||
|
} |
||||||
|
pkey = strings.ToLower(pkey) |
||||||
|
params[pkey] = pvalue |
||||||
|
s = skipSpace(s) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func skipSpace(s string) (rest string) { |
||||||
|
i := 0 |
||||||
|
for ; i < len(s); i++ { |
||||||
|
if octetTypes[s[i]]&isSpace == 0 { |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
return s[i:] |
||||||
|
} |
||||||
|
|
||||||
|
func expectToken(s string) (token, rest string) { |
||||||
|
i := 0 |
||||||
|
for ; i < len(s); i++ { |
||||||
|
if octetTypes[s[i]]&isToken == 0 { |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
return s[:i], s[i:] |
||||||
|
} |
||||||
|
|
||||||
|
func expectTokenOrQuoted(s string) (value string, rest string) { |
||||||
|
if !strings.HasPrefix(s, "\"") { |
||||||
|
return expectToken(s) |
||||||
|
} |
||||||
|
s = s[1:] |
||||||
|
for i := 0; i < len(s); i++ { |
||||||
|
switch s[i] { |
||||||
|
case '"': |
||||||
|
return s[:i], s[i+1:] |
||||||
|
case '\\': |
||||||
|
p := make([]byte, len(s)-1) |
||||||
|
j := copy(p, s[:i]) |
||||||
|
escape := true |
||||||
|
for i = i + 1; i < len(s); i++ { |
||||||
|
b := s[i] |
||||||
|
switch { |
||||||
|
case escape: |
||||||
|
escape = false |
||||||
|
p[j] = b |
||||||
|
j++ |
||||||
|
case b == '\\': |
||||||
|
escape = true |
||||||
|
case b == '"': |
||||||
|
return string(p[:j]), s[i+1:] |
||||||
|
default: |
||||||
|
p[j] = b |
||||||
|
j++ |
||||||
|
} |
||||||
|
} |
||||||
|
return "", "" |
||||||
|
} |
||||||
|
} |
||||||
|
return "", "" |
||||||
|
} |
@ -0,0 +1,530 @@ |
|||||||
|
package auth |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/docker/distribution/registry/client" |
||||||
|
"github.com/docker/distribution/registry/client/auth/challenge" |
||||||
|
"github.com/docker/distribution/registry/client/transport" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
// ErrNoBasicAuthCredentials is returned if a request can't be authorized with
|
||||||
|
// basic auth due to lack of credentials.
|
||||||
|
ErrNoBasicAuthCredentials = errors.New("no basic auth credentials") |
||||||
|
|
||||||
|
// ErrNoToken is returned if a request is successful but the body does not
|
||||||
|
// contain an authorization token.
|
||||||
|
ErrNoToken = errors.New("authorization server did not include a token in the response") |
||||||
|
) |
||||||
|
|
||||||
|
const defaultClientID = "registry-client" |
||||||
|
|
||||||
|
// AuthenticationHandler is an interface for authorizing a request from
|
||||||
|
// params from a "WWW-Authenicate" header for a single scheme.
|
||||||
|
type AuthenticationHandler interface { |
||||||
|
// Scheme returns the scheme as expected from the "WWW-Authenicate" header.
|
||||||
|
Scheme() string |
||||||
|
|
||||||
|
// AuthorizeRequest adds the authorization header to a request (if needed)
|
||||||
|
// using the parameters from "WWW-Authenticate" method. The parameters
|
||||||
|
// values depend on the scheme.
|
||||||
|
AuthorizeRequest(req *http.Request, params map[string]string) error |
||||||
|
} |
||||||
|
|
||||||
|
// CredentialStore is an interface for getting credentials for
|
||||||
|
// a given URL
|
||||||
|
type CredentialStore interface { |
||||||
|
// Basic returns basic auth for the given URL
|
||||||
|
Basic(*url.URL) (string, string) |
||||||
|
|
||||||
|
// RefreshToken returns a refresh token for the
|
||||||
|
// given URL and service
|
||||||
|
RefreshToken(*url.URL, string) string |
||||||
|
|
||||||
|
// SetRefreshToken sets the refresh token if none
|
||||||
|
// is provided for the given url and service
|
||||||
|
SetRefreshToken(realm *url.URL, service, token string) |
||||||
|
} |
||||||
|
|
||||||
|
// NewAuthorizer creates an authorizer which can handle multiple authentication
|
||||||
|
// schemes. The handlers are tried in order, the higher priority authentication
|
||||||
|
// methods should be first. The challengeMap holds a list of challenges for
|
||||||
|
// a given root API endpoint (for example "https://registry-1.docker.io/v2/").
|
||||||
|
func NewAuthorizer(manager challenge.Manager, handlers ...AuthenticationHandler) transport.RequestModifier { |
||||||
|
return &endpointAuthorizer{ |
||||||
|
challenges: manager, |
||||||
|
handlers: handlers, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type endpointAuthorizer struct { |
||||||
|
challenges challenge.Manager |
||||||
|
handlers []AuthenticationHandler |
||||||
|
} |
||||||
|
|
||||||
|
func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error { |
||||||
|
pingPath := req.URL.Path |
||||||
|
if v2Root := strings.Index(req.URL.Path, "/v2/"); v2Root != -1 { |
||||||
|
pingPath = pingPath[:v2Root+4] |
||||||
|
} else if v1Root := strings.Index(req.URL.Path, "/v1/"); v1Root != -1 { |
||||||
|
pingPath = pingPath[:v1Root] + "/v2/" |
||||||
|
} else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
ping := url.URL{ |
||||||
|
Host: req.URL.Host, |
||||||
|
Scheme: req.URL.Scheme, |
||||||
|
Path: pingPath, |
||||||
|
} |
||||||
|
|
||||||
|
challenges, err := ea.challenges.GetChallenges(ping) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if len(challenges) > 0 { |
||||||
|
for _, handler := range ea.handlers { |
||||||
|
for _, c := range challenges { |
||||||
|
if c.Scheme != handler.Scheme() { |
||||||
|
continue |
||||||
|
} |
||||||
|
if err := handler.AuthorizeRequest(req, c.Parameters); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// This is the minimum duration a token can last (in seconds).
|
||||||
|
// A token must not live less than 60 seconds because older versions
|
||||||
|
// of the Docker client didn't read their expiration from the token
|
||||||
|
// response and assumed 60 seconds. So to remain compatible with
|
||||||
|
// those implementations, a token must live at least this long.
|
||||||
|
const minimumTokenLifetimeSeconds = 60 |
||||||
|
|
||||||
|
// Private interface for time used by this package to enable tests to provide their own implementation.
|
||||||
|
type clock interface { |
||||||
|
Now() time.Time |
||||||
|
} |
||||||
|
|
||||||
|
type tokenHandler struct { |
||||||
|
creds CredentialStore |
||||||
|
transport http.RoundTripper |
||||||
|
clock clock |
||||||
|
|
||||||
|
offlineAccess bool |
||||||
|
forceOAuth bool |
||||||
|
clientID string |
||||||
|
scopes []Scope |
||||||
|
|
||||||
|
tokenLock sync.Mutex |
||||||
|
tokenCache string |
||||||
|
tokenExpiration time.Time |
||||||
|
|
||||||
|
logger Logger |
||||||
|
} |
||||||
|
|
||||||
|
// Scope is a type which is serializable to a string
|
||||||
|
// using the allow scope grammar.
|
||||||
|
type Scope interface { |
||||||
|
String() string |
||||||
|
} |
||||||
|
|
||||||
|
// RepositoryScope represents a token scope for access
|
||||||
|
// to a repository.
|
||||||
|
type RepositoryScope struct { |
||||||
|
Repository string |
||||||
|
Class string |
||||||
|
Actions []string |
||||||
|
} |
||||||
|
|
||||||
|
// String returns the string representation of the repository
|
||||||
|
// using the scope grammar
|
||||||
|
func (rs RepositoryScope) String() string { |
||||||
|
repoType := "repository" |
||||||
|
// Keep existing format for image class to maintain backwards compatibility
|
||||||
|
// with authorization servers which do not support the expanded grammar.
|
||||||
|
if rs.Class != "" && rs.Class != "image" { |
||||||
|
repoType = fmt.Sprintf("%s(%s)", repoType, rs.Class) |
||||||
|
} |
||||||
|
return fmt.Sprintf("%s:%s:%s", repoType, rs.Repository, strings.Join(rs.Actions, ",")) |
||||||
|
} |
||||||
|
|
||||||
|
// RegistryScope represents a token scope for access
|
||||||
|
// to resources in the registry.
|
||||||
|
type RegistryScope struct { |
||||||
|
Name string |
||||||
|
Actions []string |
||||||
|
} |
||||||
|
|
||||||
|
// String returns the string representation of the user
|
||||||
|
// using the scope grammar
|
||||||
|
func (rs RegistryScope) String() string { |
||||||
|
return fmt.Sprintf("registry:%s:%s", rs.Name, strings.Join(rs.Actions, ",")) |
||||||
|
} |
||||||
|
|
||||||
|
// Logger defines the injectable logging interface, used on TokenHandlers.
|
||||||
|
type Logger interface { |
||||||
|
Debugf(format string, args ...interface{}) |
||||||
|
} |
||||||
|
|
||||||
|
func logDebugf(logger Logger, format string, args ...interface{}) { |
||||||
|
if logger == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
logger.Debugf(format, args...) |
||||||
|
} |
||||||
|
|
||||||
|
// TokenHandlerOptions is used to configure a new token handler
|
||||||
|
type TokenHandlerOptions struct { |
||||||
|
Transport http.RoundTripper |
||||||
|
Credentials CredentialStore |
||||||
|
|
||||||
|
OfflineAccess bool |
||||||
|
ForceOAuth bool |
||||||
|
ClientID string |
||||||
|
Scopes []Scope |
||||||
|
Logger Logger |
||||||
|
} |
||||||
|
|
||||||
|
// An implementation of clock for providing real time data.
|
||||||
|
type realClock struct{} |
||||||
|
|
||||||
|
// Now implements clock
|
||||||
|
func (realClock) Now() time.Time { return time.Now() } |
||||||
|
|
||||||
|
// NewTokenHandler creates a new AuthenicationHandler which supports
|
||||||
|
// fetching tokens from a remote token server.
|
||||||
|
func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler { |
||||||
|
// Create options...
|
||||||
|
return NewTokenHandlerWithOptions(TokenHandlerOptions{ |
||||||
|
Transport: transport, |
||||||
|
Credentials: creds, |
||||||
|
Scopes: []Scope{ |
||||||
|
RepositoryScope{ |
||||||
|
Repository: scope, |
||||||
|
Actions: actions, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// NewTokenHandlerWithOptions creates a new token handler using the provided
|
||||||
|
// options structure.
|
||||||
|
func NewTokenHandlerWithOptions(options TokenHandlerOptions) AuthenticationHandler { |
||||||
|
handler := &tokenHandler{ |
||||||
|
transport: options.Transport, |
||||||
|
creds: options.Credentials, |
||||||
|
offlineAccess: options.OfflineAccess, |
||||||
|
forceOAuth: options.ForceOAuth, |
||||||
|
clientID: options.ClientID, |
||||||
|
scopes: options.Scopes, |
||||||
|
clock: realClock{}, |
||||||
|
logger: options.Logger, |
||||||
|
} |
||||||
|
|
||||||
|
return handler |
||||||
|
} |
||||||
|
|
||||||
|
func (th *tokenHandler) client() *http.Client { |
||||||
|
return &http.Client{ |
||||||
|
Transport: th.transport, |
||||||
|
Timeout: 15 * time.Second, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (th *tokenHandler) Scheme() string { |
||||||
|
return "bearer" |
||||||
|
} |
||||||
|
|
||||||
|
func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { |
||||||
|
var additionalScopes []string |
||||||
|
if fromParam := req.URL.Query().Get("from"); fromParam != "" { |
||||||
|
additionalScopes = append(additionalScopes, RepositoryScope{ |
||||||
|
Repository: fromParam, |
||||||
|
Actions: []string{"pull"}, |
||||||
|
}.String()) |
||||||
|
} |
||||||
|
|
||||||
|
token, err := th.getToken(params, additionalScopes...) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (th *tokenHandler) getToken(params map[string]string, additionalScopes ...string) (string, error) { |
||||||
|
th.tokenLock.Lock() |
||||||
|
defer th.tokenLock.Unlock() |
||||||
|
scopes := make([]string, 0, len(th.scopes)+len(additionalScopes)) |
||||||
|
for _, scope := range th.scopes { |
||||||
|
scopes = append(scopes, scope.String()) |
||||||
|
} |
||||||
|
var addedScopes bool |
||||||
|
for _, scope := range additionalScopes { |
||||||
|
if hasScope(scopes, scope) { |
||||||
|
continue |
||||||
|
} |
||||||
|
scopes = append(scopes, scope) |
||||||
|
addedScopes = true |
||||||
|
} |
||||||
|
|
||||||
|
now := th.clock.Now() |
||||||
|
if now.After(th.tokenExpiration) || addedScopes { |
||||||
|
token, expiration, err := th.fetchToken(params, scopes) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
// do not update cache for added scope tokens
|
||||||
|
if !addedScopes { |
||||||
|
th.tokenCache = token |
||||||
|
th.tokenExpiration = expiration |
||||||
|
} |
||||||
|
|
||||||
|
return token, nil |
||||||
|
} |
||||||
|
|
||||||
|
return th.tokenCache, nil |
||||||
|
} |
||||||
|
|
||||||
|
func hasScope(scopes []string, scope string) bool { |
||||||
|
for _, s := range scopes { |
||||||
|
if s == scope { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
type postTokenResponse struct { |
||||||
|
AccessToken string `json:"access_token"` |
||||||
|
RefreshToken string `json:"refresh_token"` |
||||||
|
ExpiresIn int `json:"expires_in"` |
||||||
|
IssuedAt time.Time `json:"issued_at"` |
||||||
|
Scope string `json:"scope"` |
||||||
|
} |
||||||
|
|
||||||
|
func (th *tokenHandler) fetchTokenWithOAuth(realm *url.URL, refreshToken, service string, scopes []string) (token string, expiration time.Time, err error) { |
||||||
|
form := url.Values{} |
||||||
|
form.Set("scope", strings.Join(scopes, " ")) |
||||||
|
form.Set("service", service) |
||||||
|
|
||||||
|
clientID := th.clientID |
||||||
|
if clientID == "" { |
||||||
|
// Use default client, this is a required field
|
||||||
|
clientID = defaultClientID |
||||||
|
} |
||||||
|
form.Set("client_id", clientID) |
||||||
|
|
||||||
|
if refreshToken != "" { |
||||||
|
form.Set("grant_type", "refresh_token") |
||||||
|
form.Set("refresh_token", refreshToken) |
||||||
|
} else if th.creds != nil { |
||||||
|
form.Set("grant_type", "password") |
||||||
|
username, password := th.creds.Basic(realm) |
||||||
|
form.Set("username", username) |
||||||
|
form.Set("password", password) |
||||||
|
|
||||||
|
// attempt to get a refresh token
|
||||||
|
form.Set("access_type", "offline") |
||||||
|
} else { |
||||||
|
// refuse to do oauth without a grant type
|
||||||
|
return "", time.Time{}, fmt.Errorf("no supported grant type") |
||||||
|
} |
||||||
|
|
||||||
|
resp, err := th.client().PostForm(realm.String(), form) |
||||||
|
if err != nil { |
||||||
|
return "", time.Time{}, err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
if !client.SuccessStatus(resp.StatusCode) { |
||||||
|
err := client.HandleErrorResponse(resp) |
||||||
|
return "", time.Time{}, err |
||||||
|
} |
||||||
|
|
||||||
|
decoder := json.NewDecoder(resp.Body) |
||||||
|
|
||||||
|
var tr postTokenResponse |
||||||
|
if err = decoder.Decode(&tr); err != nil { |
||||||
|
return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
if tr.RefreshToken != "" && tr.RefreshToken != refreshToken { |
||||||
|
th.creds.SetRefreshToken(realm, service, tr.RefreshToken) |
||||||
|
} |
||||||
|
|
||||||
|
if tr.ExpiresIn < minimumTokenLifetimeSeconds { |
||||||
|
// The default/minimum lifetime.
|
||||||
|
tr.ExpiresIn = minimumTokenLifetimeSeconds |
||||||
|
logDebugf(th.logger, "Increasing token expiration to: %d seconds", tr.ExpiresIn) |
||||||
|
} |
||||||
|
|
||||||
|
if tr.IssuedAt.IsZero() { |
||||||
|
// issued_at is optional in the token response.
|
||||||
|
tr.IssuedAt = th.clock.Now().UTC() |
||||||
|
} |
||||||
|
|
||||||
|
return tr.AccessToken, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil |
||||||
|
} |
||||||
|
|
||||||
|
type getTokenResponse struct { |
||||||
|
Token string `json:"token"` |
||||||
|
AccessToken string `json:"access_token"` |
||||||
|
ExpiresIn int `json:"expires_in"` |
||||||
|
IssuedAt time.Time `json:"issued_at"` |
||||||
|
RefreshToken string `json:"refresh_token"` |
||||||
|
} |
||||||
|
|
||||||
|
func (th *tokenHandler) fetchTokenWithBasicAuth(realm *url.URL, service string, scopes []string) (token string, expiration time.Time, err error) { |
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", realm.String(), nil) |
||||||
|
if err != nil { |
||||||
|
return "", time.Time{}, err |
||||||
|
} |
||||||
|
|
||||||
|
reqParams := req.URL.Query() |
||||||
|
|
||||||
|
if service != "" { |
||||||
|
reqParams.Add("service", service) |
||||||
|
} |
||||||
|
|
||||||
|
for _, scope := range scopes { |
||||||
|
reqParams.Add("scope", scope) |
||||||
|
} |
||||||
|
|
||||||
|
if th.offlineAccess { |
||||||
|
reqParams.Add("offline_token", "true") |
||||||
|
clientID := th.clientID |
||||||
|
if clientID == "" { |
||||||
|
clientID = defaultClientID |
||||||
|
} |
||||||
|
reqParams.Add("client_id", clientID) |
||||||
|
} |
||||||
|
|
||||||
|
if th.creds != nil { |
||||||
|
username, password := th.creds.Basic(realm) |
||||||
|
if username != "" && password != "" { |
||||||
|
reqParams.Add("account", username) |
||||||
|
req.SetBasicAuth(username, password) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
req.URL.RawQuery = reqParams.Encode() |
||||||
|
|
||||||
|
resp, err := th.client().Do(req) |
||||||
|
if err != nil { |
||||||
|
return "", time.Time{}, err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
if !client.SuccessStatus(resp.StatusCode) { |
||||||
|
err := client.HandleErrorResponse(resp) |
||||||
|
return "", time.Time{}, err |
||||||
|
} |
||||||
|
|
||||||
|
decoder := json.NewDecoder(resp.Body) |
||||||
|
|
||||||
|
var tr getTokenResponse |
||||||
|
if err = decoder.Decode(&tr); err != nil { |
||||||
|
return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
if tr.RefreshToken != "" && th.creds != nil { |
||||||
|
th.creds.SetRefreshToken(realm, service, tr.RefreshToken) |
||||||
|
} |
||||||
|
|
||||||
|
// `access_token` is equivalent to `token` and if both are specified
|
||||||
|
// the choice is undefined. Canonicalize `access_token` by sticking
|
||||||
|
// things in `token`.
|
||||||
|
if tr.AccessToken != "" { |
||||||
|
tr.Token = tr.AccessToken |
||||||
|
} |
||||||
|
|
||||||
|
if tr.Token == "" { |
||||||
|
return "", time.Time{}, ErrNoToken |
||||||
|
} |
||||||
|
|
||||||
|
if tr.ExpiresIn < minimumTokenLifetimeSeconds { |
||||||
|
// The default/minimum lifetime.
|
||||||
|
tr.ExpiresIn = minimumTokenLifetimeSeconds |
||||||
|
logDebugf(th.logger, "Increasing token expiration to: %d seconds", tr.ExpiresIn) |
||||||
|
} |
||||||
|
|
||||||
|
if tr.IssuedAt.IsZero() { |
||||||
|
// issued_at is optional in the token response.
|
||||||
|
tr.IssuedAt = th.clock.Now().UTC() |
||||||
|
} |
||||||
|
|
||||||
|
return tr.Token, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (th *tokenHandler) fetchToken(params map[string]string, scopes []string) (token string, expiration time.Time, err error) { |
||||||
|
realm, ok := params["realm"] |
||||||
|
if !ok { |
||||||
|
return "", time.Time{}, errors.New("no realm specified for token auth challenge") |
||||||
|
} |
||||||
|
|
||||||
|
// TODO(dmcgowan): Handle empty scheme and relative realm
|
||||||
|
realmURL, err := url.Parse(realm) |
||||||
|
if err != nil { |
||||||
|
return "", time.Time{}, fmt.Errorf("invalid token auth challenge realm: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
service := params["service"] |
||||||
|
|
||||||
|
var refreshToken string |
||||||
|
|
||||||
|
if th.creds != nil { |
||||||
|
refreshToken = th.creds.RefreshToken(realmURL, service) |
||||||
|
} |
||||||
|
|
||||||
|
if refreshToken != "" || th.forceOAuth { |
||||||
|
return th.fetchTokenWithOAuth(realmURL, refreshToken, service, scopes) |
||||||
|
} |
||||||
|
|
||||||
|
return th.fetchTokenWithBasicAuth(realmURL, service, scopes) |
||||||
|
} |
||||||
|
|
||||||
|
type basicHandler struct { |
||||||
|
creds CredentialStore |
||||||
|
} |
||||||
|
|
||||||
|
// NewBasicHandler creaters a new authentiation handler which adds
|
||||||
|
// basic authentication credentials to a request.
|
||||||
|
func NewBasicHandler(creds CredentialStore) AuthenticationHandler { |
||||||
|
return &basicHandler{ |
||||||
|
creds: creds, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (*basicHandler) Scheme() string { |
||||||
|
return "basic" |
||||||
|
} |
||||||
|
|
||||||
|
func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { |
||||||
|
if bh.creds != nil { |
||||||
|
username, password := bh.creds.Basic(req.URL) |
||||||
|
if username != "" && password != "" { |
||||||
|
req.SetBasicAuth(username, password) |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
return ErrNoBasicAuthCredentials |
||||||
|
} |
@ -0,0 +1,162 @@ |
|||||||
|
package client |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"io/ioutil" |
||||||
|
"net/http" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/docker/distribution" |
||||||
|
) |
||||||
|
|
||||||
|
type httpBlobUpload struct { |
||||||
|
statter distribution.BlobStatter |
||||||
|
client *http.Client |
||||||
|
|
||||||
|
uuid string |
||||||
|
startedAt time.Time |
||||||
|
|
||||||
|
location string // always the last value of the location header.
|
||||||
|
offset int64 |
||||||
|
closed bool |
||||||
|
} |
||||||
|
|
||||||
|
func (hbu *httpBlobUpload) Reader() (io.ReadCloser, error) { |
||||||
|
panic("Not implemented") |
||||||
|
} |
||||||
|
|
||||||
|
func (hbu *httpBlobUpload) handleErrorResponse(resp *http.Response) error { |
||||||
|
if resp.StatusCode == http.StatusNotFound { |
||||||
|
return distribution.ErrBlobUploadUnknown |
||||||
|
} |
||||||
|
return HandleErrorResponse(resp) |
||||||
|
} |
||||||
|
|
||||||
|
func (hbu *httpBlobUpload) ReadFrom(r io.Reader) (n int64, err error) { |
||||||
|
req, err := http.NewRequest("PATCH", hbu.location, ioutil.NopCloser(r)) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
defer req.Body.Close() |
||||||
|
|
||||||
|
resp, err := hbu.client.Do(req) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
|
||||||
|
if !SuccessStatus(resp.StatusCode) { |
||||||
|
return 0, hbu.handleErrorResponse(resp) |
||||||
|
} |
||||||
|
|
||||||
|
hbu.uuid = resp.Header.Get("Docker-Upload-UUID") |
||||||
|
hbu.location, err = sanitizeLocation(resp.Header.Get("Location"), hbu.location) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
rng := resp.Header.Get("Range") |
||||||
|
var start, end int64 |
||||||
|
if n, err := fmt.Sscanf(rng, "%d-%d", &start, &end); err != nil { |
||||||
|
return 0, err |
||||||
|
} else if n != 2 || end < start { |
||||||
|
return 0, fmt.Errorf("bad range format: %s", rng) |
||||||
|
} |
||||||
|
|
||||||
|
return (end - start + 1), nil |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func (hbu *httpBlobUpload) Write(p []byte) (n int, err error) { |
||||||
|
req, err := http.NewRequest("PATCH", hbu.location, bytes.NewReader(p)) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
req.Header.Set("Content-Range", fmt.Sprintf("%d-%d", hbu.offset, hbu.offset+int64(len(p)-1))) |
||||||
|
req.Header.Set("Content-Length", fmt.Sprintf("%d", len(p))) |
||||||
|
req.Header.Set("Content-Type", "application/octet-stream") |
||||||
|
|
||||||
|
resp, err := hbu.client.Do(req) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
|
||||||
|
if !SuccessStatus(resp.StatusCode) { |
||||||
|
return 0, hbu.handleErrorResponse(resp) |
||||||
|
} |
||||||
|
|
||||||
|
hbu.uuid = resp.Header.Get("Docker-Upload-UUID") |
||||||
|
hbu.location, err = sanitizeLocation(resp.Header.Get("Location"), hbu.location) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
rng := resp.Header.Get("Range") |
||||||
|
var start, end int |
||||||
|
if n, err := fmt.Sscanf(rng, "%d-%d", &start, &end); err != nil { |
||||||
|
return 0, err |
||||||
|
} else if n != 2 || end < start { |
||||||
|
return 0, fmt.Errorf("bad range format: %s", rng) |
||||||
|
} |
||||||
|
|
||||||
|
return (end - start + 1), nil |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func (hbu *httpBlobUpload) Size() int64 { |
||||||
|
return hbu.offset |
||||||
|
} |
||||||
|
|
||||||
|
func (hbu *httpBlobUpload) ID() string { |
||||||
|
return hbu.uuid |
||||||
|
} |
||||||
|
|
||||||
|
func (hbu *httpBlobUpload) StartedAt() time.Time { |
||||||
|
return hbu.startedAt |
||||||
|
} |
||||||
|
|
||||||
|
func (hbu *httpBlobUpload) Commit(ctx context.Context, desc distribution.Descriptor) (distribution.Descriptor, error) { |
||||||
|
// TODO(dmcgowan): Check if already finished, if so just fetch
|
||||||
|
req, err := http.NewRequest("PUT", hbu.location, nil) |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
|
||||||
|
values := req.URL.Query() |
||||||
|
values.Set("digest", desc.Digest.String()) |
||||||
|
req.URL.RawQuery = values.Encode() |
||||||
|
|
||||||
|
resp, err := hbu.client.Do(req) |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
if !SuccessStatus(resp.StatusCode) { |
||||||
|
return distribution.Descriptor{}, hbu.handleErrorResponse(resp) |
||||||
|
} |
||||||
|
|
||||||
|
return hbu.statter.Stat(ctx, desc.Digest) |
||||||
|
} |
||||||
|
|
||||||
|
func (hbu *httpBlobUpload) Cancel(ctx context.Context) error { |
||||||
|
req, err := http.NewRequest("DELETE", hbu.location, nil) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
resp, err := hbu.client.Do(req) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNotFound || SuccessStatus(resp.StatusCode) { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return hbu.handleErrorResponse(resp) |
||||||
|
} |
||||||
|
|
||||||
|
func (hbu *httpBlobUpload) Close() error { |
||||||
|
hbu.closed = true |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,139 @@ |
|||||||
|
package client |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"io/ioutil" |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"github.com/docker/distribution/registry/api/errcode" |
||||||
|
"github.com/docker/distribution/registry/client/auth/challenge" |
||||||
|
) |
||||||
|
|
||||||
|
// ErrNoErrorsInBody is returned when an HTTP response body parses to an empty
|
||||||
|
// errcode.Errors slice.
|
||||||
|
var ErrNoErrorsInBody = errors.New("no error details found in HTTP response body") |
||||||
|
|
||||||
|
// UnexpectedHTTPStatusError is returned when an unexpected HTTP status is
|
||||||
|
// returned when making a registry api call.
|
||||||
|
type UnexpectedHTTPStatusError struct { |
||||||
|
Status string |
||||||
|
} |
||||||
|
|
||||||
|
func (e *UnexpectedHTTPStatusError) Error() string { |
||||||
|
return fmt.Sprintf("received unexpected HTTP status: %s", e.Status) |
||||||
|
} |
||||||
|
|
||||||
|
// UnexpectedHTTPResponseError is returned when an expected HTTP status code
|
||||||
|
// is returned, but the content was unexpected and failed to be parsed.
|
||||||
|
type UnexpectedHTTPResponseError struct { |
||||||
|
ParseErr error |
||||||
|
StatusCode int |
||||||
|
Response []byte |
||||||
|
} |
||||||
|
|
||||||
|
func (e *UnexpectedHTTPResponseError) Error() string { |
||||||
|
return fmt.Sprintf("error parsing HTTP %d response body: %s: %q", e.StatusCode, e.ParseErr.Error(), string(e.Response)) |
||||||
|
} |
||||||
|
|
||||||
|
func parseHTTPErrorResponse(statusCode int, r io.Reader) error { |
||||||
|
var errors errcode.Errors |
||||||
|
body, err := ioutil.ReadAll(r) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// For backward compatibility, handle irregularly formatted
|
||||||
|
// messages that contain a "details" field.
|
||||||
|
var detailsErr struct { |
||||||
|
Details string `json:"details"` |
||||||
|
} |
||||||
|
err = json.Unmarshal(body, &detailsErr) |
||||||
|
if err == nil && detailsErr.Details != "" { |
||||||
|
switch statusCode { |
||||||
|
case http.StatusUnauthorized: |
||||||
|
return errcode.ErrorCodeUnauthorized.WithMessage(detailsErr.Details) |
||||||
|
case http.StatusTooManyRequests: |
||||||
|
return errcode.ErrorCodeTooManyRequests.WithMessage(detailsErr.Details) |
||||||
|
default: |
||||||
|
return errcode.ErrorCodeUnknown.WithMessage(detailsErr.Details) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &errors); err != nil { |
||||||
|
return &UnexpectedHTTPResponseError{ |
||||||
|
ParseErr: err, |
||||||
|
StatusCode: statusCode, |
||||||
|
Response: body, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(errors) == 0 { |
||||||
|
// If there was no error specified in the body, return
|
||||||
|
// UnexpectedHTTPResponseError.
|
||||||
|
return &UnexpectedHTTPResponseError{ |
||||||
|
ParseErr: ErrNoErrorsInBody, |
||||||
|
StatusCode: statusCode, |
||||||
|
Response: body, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return errors |
||||||
|
} |
||||||
|
|
||||||
|
func makeErrorList(err error) []error { |
||||||
|
if errL, ok := err.(errcode.Errors); ok { |
||||||
|
return []error(errL) |
||||||
|
} |
||||||
|
return []error{err} |
||||||
|
} |
||||||
|
|
||||||
|
func mergeErrors(err1, err2 error) error { |
||||||
|
return errcode.Errors(append(makeErrorList(err1), makeErrorList(err2)...)) |
||||||
|
} |
||||||
|
|
||||||
|
// HandleErrorResponse returns error parsed from HTTP response for an
|
||||||
|
// unsuccessful HTTP response code (in the range 400 - 499 inclusive). An
|
||||||
|
// UnexpectedHTTPStatusError returned for response code outside of expected
|
||||||
|
// range.
|
||||||
|
func HandleErrorResponse(resp *http.Response) error { |
||||||
|
if resp.StatusCode >= 400 && resp.StatusCode < 500 { |
||||||
|
// Check for OAuth errors within the `WWW-Authenticate` header first
|
||||||
|
// See https://tools.ietf.org/html/rfc6750#section-3
|
||||||
|
for _, c := range challenge.ResponseChallenges(resp) { |
||||||
|
if c.Scheme == "bearer" { |
||||||
|
var err errcode.Error |
||||||
|
// codes defined at https://tools.ietf.org/html/rfc6750#section-3.1
|
||||||
|
switch c.Parameters["error"] { |
||||||
|
case "invalid_token": |
||||||
|
err.Code = errcode.ErrorCodeUnauthorized |
||||||
|
case "insufficient_scope": |
||||||
|
err.Code = errcode.ErrorCodeDenied |
||||||
|
default: |
||||||
|
continue |
||||||
|
} |
||||||
|
if description := c.Parameters["error_description"]; description != "" { |
||||||
|
err.Message = description |
||||||
|
} else { |
||||||
|
err.Message = err.Code.Message() |
||||||
|
} |
||||||
|
|
||||||
|
return mergeErrors(err, parseHTTPErrorResponse(resp.StatusCode, resp.Body)) |
||||||
|
} |
||||||
|
} |
||||||
|
err := parseHTTPErrorResponse(resp.StatusCode, resp.Body) |
||||||
|
if uErr, ok := err.(*UnexpectedHTTPResponseError); ok && resp.StatusCode == 401 { |
||||||
|
return errcode.ErrorCodeUnauthorized.WithDetail(uErr.Response) |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
return &UnexpectedHTTPStatusError{Status: resp.Status} |
||||||
|
} |
||||||
|
|
||||||
|
// SuccessStatus returns true if the argument is a successful HTTP response
|
||||||
|
// code (in the range 200 - 399 inclusive).
|
||||||
|
func SuccessStatus(status int) bool { |
||||||
|
return status >= 200 && status <= 399 |
||||||
|
} |
@ -0,0 +1,867 @@ |
|||||||
|
package client |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"context" |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"io/ioutil" |
||||||
|
"net/http" |
||||||
|
"net/url" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/docker/distribution" |
||||||
|
"github.com/docker/distribution/reference" |
||||||
|
"github.com/docker/distribution/registry/api/v2" |
||||||
|
"github.com/docker/distribution/registry/client/transport" |
||||||
|
"github.com/docker/distribution/registry/storage/cache" |
||||||
|
"github.com/docker/distribution/registry/storage/cache/memory" |
||||||
|
"github.com/opencontainers/go-digest" |
||||||
|
) |
||||||
|
|
||||||
|
// Registry provides an interface for calling Repositories, which returns a catalog of repositories.
|
||||||
|
type Registry interface { |
||||||
|
Repositories(ctx context.Context, repos []string, last string) (n int, err error) |
||||||
|
} |
||||||
|
|
||||||
|
// checkHTTPRedirect is a callback that can manipulate redirected HTTP
|
||||||
|
// requests. It is used to preserve Accept and Range headers.
|
||||||
|
func checkHTTPRedirect(req *http.Request, via []*http.Request) error { |
||||||
|
if len(via) >= 10 { |
||||||
|
return errors.New("stopped after 10 redirects") |
||||||
|
} |
||||||
|
|
||||||
|
if len(via) > 0 { |
||||||
|
for headerName, headerVals := range via[0].Header { |
||||||
|
if headerName != "Accept" && headerName != "Range" { |
||||||
|
continue |
||||||
|
} |
||||||
|
for _, val := range headerVals { |
||||||
|
// Don't add to redirected request if redirected
|
||||||
|
// request already has a header with the same
|
||||||
|
// name and value.
|
||||||
|
hasValue := false |
||||||
|
for _, existingVal := range req.Header[headerName] { |
||||||
|
if existingVal == val { |
||||||
|
hasValue = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if !hasValue { |
||||||
|
req.Header.Add(headerName, val) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// NewRegistry creates a registry namespace which can be used to get a listing of repositories
|
||||||
|
func NewRegistry(baseURL string, transport http.RoundTripper) (Registry, error) { |
||||||
|
ub, err := v2.NewURLBuilderFromString(baseURL, false) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
client := &http.Client{ |
||||||
|
Transport: transport, |
||||||
|
Timeout: 1 * time.Minute, |
||||||
|
CheckRedirect: checkHTTPRedirect, |
||||||
|
} |
||||||
|
|
||||||
|
return ®istry{ |
||||||
|
client: client, |
||||||
|
ub: ub, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
type registry struct { |
||||||
|
client *http.Client |
||||||
|
ub *v2.URLBuilder |
||||||
|
} |
||||||
|
|
||||||
|
// Repositories returns a lexigraphically sorted catalog given a base URL. The 'entries' slice will be filled up to the size
|
||||||
|
// of the slice, starting at the value provided in 'last'. The number of entries will be returned along with io.EOF if there
|
||||||
|
// are no more entries
|
||||||
|
func (r *registry) Repositories(ctx context.Context, entries []string, last string) (int, error) { |
||||||
|
var numFilled int |
||||||
|
var returnErr error |
||||||
|
|
||||||
|
values := buildCatalogValues(len(entries), last) |
||||||
|
u, err := r.ub.BuildCatalogURL(values) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
|
||||||
|
resp, err := r.client.Get(u) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
if SuccessStatus(resp.StatusCode) { |
||||||
|
var ctlg struct { |
||||||
|
Repositories []string `json:"repositories"` |
||||||
|
} |
||||||
|
decoder := json.NewDecoder(resp.Body) |
||||||
|
|
||||||
|
if err := decoder.Decode(&ctlg); err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
|
||||||
|
for cnt := range ctlg.Repositories { |
||||||
|
entries[cnt] = ctlg.Repositories[cnt] |
||||||
|
} |
||||||
|
numFilled = len(ctlg.Repositories) |
||||||
|
|
||||||
|
link := resp.Header.Get("Link") |
||||||
|
if link == "" { |
||||||
|
returnErr = io.EOF |
||||||
|
} |
||||||
|
} else { |
||||||
|
return 0, HandleErrorResponse(resp) |
||||||
|
} |
||||||
|
|
||||||
|
return numFilled, returnErr |
||||||
|
} |
||||||
|
|
||||||
|
// NewRepository creates a new Repository for the given repository name and base URL.
|
||||||
|
func NewRepository(name reference.Named, baseURL string, transport http.RoundTripper) (distribution.Repository, error) { |
||||||
|
ub, err := v2.NewURLBuilderFromString(baseURL, false) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
client := &http.Client{ |
||||||
|
Transport: transport, |
||||||
|
CheckRedirect: checkHTTPRedirect, |
||||||
|
// TODO(dmcgowan): create cookie jar
|
||||||
|
} |
||||||
|
|
||||||
|
return &repository{ |
||||||
|
client: client, |
||||||
|
ub: ub, |
||||||
|
name: name, |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
type repository struct { |
||||||
|
client *http.Client |
||||||
|
ub *v2.URLBuilder |
||||||
|
name reference.Named |
||||||
|
} |
||||||
|
|
||||||
|
func (r *repository) Named() reference.Named { |
||||||
|
return r.name |
||||||
|
} |
||||||
|
|
||||||
|
func (r *repository) Blobs(ctx context.Context) distribution.BlobStore { |
||||||
|
statter := &blobStatter{ |
||||||
|
name: r.name, |
||||||
|
ub: r.ub, |
||||||
|
client: r.client, |
||||||
|
} |
||||||
|
return &blobs{ |
||||||
|
name: r.name, |
||||||
|
ub: r.ub, |
||||||
|
client: r.client, |
||||||
|
statter: cache.NewCachedBlobStatter(memory.NewInMemoryBlobDescriptorCacheProvider(), statter), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (r *repository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) { |
||||||
|
// todo(richardscothern): options should be sent over the wire
|
||||||
|
return &manifests{ |
||||||
|
name: r.name, |
||||||
|
ub: r.ub, |
||||||
|
client: r.client, |
||||||
|
etags: make(map[string]string), |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (r *repository) Tags(ctx context.Context) distribution.TagService { |
||||||
|
return &tags{ |
||||||
|
client: r.client, |
||||||
|
ub: r.ub, |
||||||
|
name: r.Named(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// tags implements remote tagging operations.
|
||||||
|
type tags struct { |
||||||
|
client *http.Client |
||||||
|
ub *v2.URLBuilder |
||||||
|
name reference.Named |
||||||
|
} |
||||||
|
|
||||||
|
// All returns all tags
|
||||||
|
func (t *tags) All(ctx context.Context) ([]string, error) { |
||||||
|
var tags []string |
||||||
|
|
||||||
|
listURLStr, err := t.ub.BuildTagsURL(t.name) |
||||||
|
if err != nil { |
||||||
|
return tags, err |
||||||
|
} |
||||||
|
|
||||||
|
listURL, err := url.Parse(listURLStr) |
||||||
|
if err != nil { |
||||||
|
return tags, err |
||||||
|
} |
||||||
|
|
||||||
|
for { |
||||||
|
resp, err := t.client.Get(listURL.String()) |
||||||
|
if err != nil { |
||||||
|
return tags, err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
if SuccessStatus(resp.StatusCode) { |
||||||
|
b, err := ioutil.ReadAll(resp.Body) |
||||||
|
if err != nil { |
||||||
|
return tags, err |
||||||
|
} |
||||||
|
|
||||||
|
tagsResponse := struct { |
||||||
|
Tags []string `json:"tags"` |
||||||
|
}{} |
||||||
|
if err := json.Unmarshal(b, &tagsResponse); err != nil { |
||||||
|
return tags, err |
||||||
|
} |
||||||
|
tags = append(tags, tagsResponse.Tags...) |
||||||
|
if link := resp.Header.Get("Link"); link != "" { |
||||||
|
linkURLStr := strings.Trim(strings.Split(link, ";")[0], "<>") |
||||||
|
linkURL, err := url.Parse(linkURLStr) |
||||||
|
if err != nil { |
||||||
|
return tags, err |
||||||
|
} |
||||||
|
|
||||||
|
listURL = listURL.ResolveReference(linkURL) |
||||||
|
} else { |
||||||
|
return tags, nil |
||||||
|
} |
||||||
|
} else { |
||||||
|
return tags, HandleErrorResponse(resp) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func descriptorFromResponse(response *http.Response) (distribution.Descriptor, error) { |
||||||
|
desc := distribution.Descriptor{} |
||||||
|
headers := response.Header |
||||||
|
|
||||||
|
ctHeader := headers.Get("Content-Type") |
||||||
|
if ctHeader == "" { |
||||||
|
return distribution.Descriptor{}, errors.New("missing or empty Content-Type header") |
||||||
|
} |
||||||
|
desc.MediaType = ctHeader |
||||||
|
|
||||||
|
digestHeader := headers.Get("Docker-Content-Digest") |
||||||
|
if digestHeader == "" { |
||||||
|
bytes, err := ioutil.ReadAll(response.Body) |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
_, desc, err := distribution.UnmarshalManifest(ctHeader, bytes) |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
return desc, nil |
||||||
|
} |
||||||
|
|
||||||
|
dgst, err := digest.Parse(digestHeader) |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
desc.Digest = dgst |
||||||
|
|
||||||
|
lengthHeader := headers.Get("Content-Length") |
||||||
|
if lengthHeader == "" { |
||||||
|
return distribution.Descriptor{}, errors.New("missing or empty Content-Length header") |
||||||
|
} |
||||||
|
length, err := strconv.ParseInt(lengthHeader, 10, 64) |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
desc.Size = length |
||||||
|
|
||||||
|
return desc, nil |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// Get issues a HEAD request for a Manifest against its named endpoint in order
|
||||||
|
// to construct a descriptor for the tag. If the registry doesn't support HEADing
|
||||||
|
// a manifest, fallback to GET.
|
||||||
|
func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { |
||||||
|
ref, err := reference.WithTag(t.name, tag) |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
u, err := t.ub.BuildManifestURL(ref) |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
|
||||||
|
newRequest := func(method string) (*http.Response, error) { |
||||||
|
req, err := http.NewRequest(method, u, nil) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
for _, t := range distribution.ManifestMediaTypes() { |
||||||
|
req.Header.Add("Accept", t) |
||||||
|
} |
||||||
|
resp, err := t.client.Do(req) |
||||||
|
return resp, err |
||||||
|
} |
||||||
|
|
||||||
|
resp, err := newRequest("HEAD") |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
switch { |
||||||
|
case resp.StatusCode >= 200 && resp.StatusCode < 400 && len(resp.Header.Get("Docker-Content-Digest")) > 0: |
||||||
|
// if the response is a success AND a Docker-Content-Digest can be retrieved from the headers
|
||||||
|
return descriptorFromResponse(resp) |
||||||
|
default: |
||||||
|
// if the response is an error - there will be no body to decode.
|
||||||
|
// Issue a GET request:
|
||||||
|
// - for data from a server that does not handle HEAD
|
||||||
|
// - to get error details in case of a failure
|
||||||
|
resp, err = newRequest("GET") |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
if resp.StatusCode >= 200 && resp.StatusCode < 400 { |
||||||
|
return descriptorFromResponse(resp) |
||||||
|
} |
||||||
|
return distribution.Descriptor{}, HandleErrorResponse(resp) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (t *tags) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) { |
||||||
|
panic("not implemented") |
||||||
|
} |
||||||
|
|
||||||
|
func (t *tags) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error { |
||||||
|
panic("not implemented") |
||||||
|
} |
||||||
|
|
||||||
|
func (t *tags) Untag(ctx context.Context, tag string) error { |
||||||
|
panic("not implemented") |
||||||
|
} |
||||||
|
|
||||||
|
type manifests struct { |
||||||
|
name reference.Named |
||||||
|
ub *v2.URLBuilder |
||||||
|
client *http.Client |
||||||
|
etags map[string]string |
||||||
|
} |
||||||
|
|
||||||
|
func (ms *manifests) Exists(ctx context.Context, dgst digest.Digest) (bool, error) { |
||||||
|
ref, err := reference.WithDigest(ms.name, dgst) |
||||||
|
if err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
u, err := ms.ub.BuildManifestURL(ref) |
||||||
|
if err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
|
||||||
|
resp, err := ms.client.Head(u) |
||||||
|
if err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
|
||||||
|
if SuccessStatus(resp.StatusCode) { |
||||||
|
return true, nil |
||||||
|
} else if resp.StatusCode == http.StatusNotFound { |
||||||
|
return false, nil |
||||||
|
} |
||||||
|
return false, HandleErrorResponse(resp) |
||||||
|
} |
||||||
|
|
||||||
|
// AddEtagToTag allows a client to supply an eTag to Get which will be
|
||||||
|
// used for a conditional HTTP request. If the eTag matches, a nil manifest
|
||||||
|
// and ErrManifestNotModified error will be returned. etag is automatically
|
||||||
|
// quoted when added to this map.
|
||||||
|
func AddEtagToTag(tag, etag string) distribution.ManifestServiceOption { |
||||||
|
return etagOption{tag, etag} |
||||||
|
} |
||||||
|
|
||||||
|
type etagOption struct{ tag, etag string } |
||||||
|
|
||||||
|
func (o etagOption) Apply(ms distribution.ManifestService) error { |
||||||
|
if ms, ok := ms.(*manifests); ok { |
||||||
|
ms.etags[o.tag] = fmt.Sprintf(`"%s"`, o.etag) |
||||||
|
return nil |
||||||
|
} |
||||||
|
return fmt.Errorf("etag options is a client-only option") |
||||||
|
} |
||||||
|
|
||||||
|
// ReturnContentDigest allows a client to set a the content digest on
|
||||||
|
// a successful request from the 'Docker-Content-Digest' header. This
|
||||||
|
// returned digest is represents the digest which the registry uses
|
||||||
|
// to refer to the content and can be used to delete the content.
|
||||||
|
func ReturnContentDigest(dgst *digest.Digest) distribution.ManifestServiceOption { |
||||||
|
return contentDigestOption{dgst} |
||||||
|
} |
||||||
|
|
||||||
|
type contentDigestOption struct{ digest *digest.Digest } |
||||||
|
|
||||||
|
func (o contentDigestOption) Apply(ms distribution.ManifestService) error { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { |
||||||
|
var ( |
||||||
|
digestOrTag string |
||||||
|
ref reference.Named |
||||||
|
err error |
||||||
|
contentDgst *digest.Digest |
||||||
|
mediaTypes []string |
||||||
|
) |
||||||
|
|
||||||
|
for _, option := range options { |
||||||
|
switch opt := option.(type) { |
||||||
|
case distribution.WithTagOption: |
||||||
|
digestOrTag = opt.Tag |
||||||
|
ref, err = reference.WithTag(ms.name, opt.Tag) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
case contentDigestOption: |
||||||
|
contentDgst = opt.digest |
||||||
|
case distribution.WithManifestMediaTypesOption: |
||||||
|
mediaTypes = opt.MediaTypes |
||||||
|
default: |
||||||
|
err := option.Apply(ms) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if digestOrTag == "" { |
||||||
|
digestOrTag = dgst.String() |
||||||
|
ref, err = reference.WithDigest(ms.name, dgst) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if len(mediaTypes) == 0 { |
||||||
|
mediaTypes = distribution.ManifestMediaTypes() |
||||||
|
} |
||||||
|
|
||||||
|
u, err := ms.ub.BuildManifestURL(ref) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", u, nil) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
for _, t := range mediaTypes { |
||||||
|
req.Header.Add("Accept", t) |
||||||
|
} |
||||||
|
|
||||||
|
if _, ok := ms.etags[digestOrTag]; ok { |
||||||
|
req.Header.Set("If-None-Match", ms.etags[digestOrTag]) |
||||||
|
} |
||||||
|
|
||||||
|
resp, err := ms.client.Do(req) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
if resp.StatusCode == http.StatusNotModified { |
||||||
|
return nil, distribution.ErrManifestNotModified |
||||||
|
} else if SuccessStatus(resp.StatusCode) { |
||||||
|
if contentDgst != nil { |
||||||
|
dgst, err := digest.Parse(resp.Header.Get("Docker-Content-Digest")) |
||||||
|
if err == nil { |
||||||
|
*contentDgst = dgst |
||||||
|
} |
||||||
|
} |
||||||
|
mt := resp.Header.Get("Content-Type") |
||||||
|
body, err := ioutil.ReadAll(resp.Body) |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
m, _, err := distribution.UnmarshalManifest(mt, body) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return m, nil |
||||||
|
} |
||||||
|
return nil, HandleErrorResponse(resp) |
||||||
|
} |
||||||
|
|
||||||
|
// Put puts a manifest. A tag can be specified using an options parameter which uses some shared state to hold the
|
||||||
|
// tag name in order to build the correct upload URL.
|
||||||
|
func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { |
||||||
|
ref := ms.name |
||||||
|
var tagged bool |
||||||
|
|
||||||
|
for _, option := range options { |
||||||
|
if opt, ok := option.(distribution.WithTagOption); ok { |
||||||
|
var err error |
||||||
|
ref, err = reference.WithTag(ref, opt.Tag) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
tagged = true |
||||||
|
} else { |
||||||
|
err := option.Apply(ms) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
mediaType, p, err := m.Payload() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
if !tagged { |
||||||
|
// generate a canonical digest and Put by digest
|
||||||
|
_, d, err := distribution.UnmarshalManifest(mediaType, p) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
ref, err = reference.WithDigest(ref, d.Digest) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
manifestURL, err := ms.ub.BuildManifestURL(ref) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(p)) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
putRequest.Header.Set("Content-Type", mediaType) |
||||||
|
|
||||||
|
resp, err := ms.client.Do(putRequest) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
if SuccessStatus(resp.StatusCode) { |
||||||
|
dgstHeader := resp.Header.Get("Docker-Content-Digest") |
||||||
|
dgst, err := digest.Parse(dgstHeader) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return dgst, nil |
||||||
|
} |
||||||
|
|
||||||
|
return "", HandleErrorResponse(resp) |
||||||
|
} |
||||||
|
|
||||||
|
func (ms *manifests) Delete(ctx context.Context, dgst digest.Digest) error { |
||||||
|
ref, err := reference.WithDigest(ms.name, dgst) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
u, err := ms.ub.BuildManifestURL(ref) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
req, err := http.NewRequest("DELETE", u, nil) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
resp, err := ms.client.Do(req) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
if SuccessStatus(resp.StatusCode) { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return HandleErrorResponse(resp) |
||||||
|
} |
||||||
|
|
||||||
|
// todo(richardscothern): Restore interface and implementation with merge of #1050
|
||||||
|
/*func (ms *manifests) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) { |
||||||
|
panic("not supported") |
||||||
|
}*/ |
||||||
|
|
||||||
|
type blobs struct { |
||||||
|
name reference.Named |
||||||
|
ub *v2.URLBuilder |
||||||
|
client *http.Client |
||||||
|
|
||||||
|
statter distribution.BlobDescriptorService |
||||||
|
distribution.BlobDeleter |
||||||
|
} |
||||||
|
|
||||||
|
func sanitizeLocation(location, base string) (string, error) { |
||||||
|
baseURL, err := url.Parse(base) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
locationURL, err := url.Parse(location) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
return baseURL.ResolveReference(locationURL).String(), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (bs *blobs) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { |
||||||
|
return bs.statter.Stat(ctx, dgst) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func (bs *blobs) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) { |
||||||
|
reader, err := bs.Open(ctx, dgst) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer reader.Close() |
||||||
|
|
||||||
|
return ioutil.ReadAll(reader) |
||||||
|
} |
||||||
|
|
||||||
|
func (bs *blobs) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) { |
||||||
|
ref, err := reference.WithDigest(bs.name, dgst) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
blobURL, err := bs.ub.BuildBlobURL(ref) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return transport.NewHTTPReadSeeker(bs.client, blobURL, |
||||||
|
func(resp *http.Response) error { |
||||||
|
if resp.StatusCode == http.StatusNotFound { |
||||||
|
return distribution.ErrBlobUnknown |
||||||
|
} |
||||||
|
return HandleErrorResponse(resp) |
||||||
|
}), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (bs *blobs) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error { |
||||||
|
panic("not implemented") |
||||||
|
} |
||||||
|
|
||||||
|
func (bs *blobs) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) { |
||||||
|
writer, err := bs.Create(ctx) |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
dgstr := digest.Canonical.Digester() |
||||||
|
n, err := io.Copy(writer, io.TeeReader(bytes.NewReader(p), dgstr.Hash())) |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
if n < int64(len(p)) { |
||||||
|
return distribution.Descriptor{}, fmt.Errorf("short copy: wrote %d of %d", n, len(p)) |
||||||
|
} |
||||||
|
|
||||||
|
desc := distribution.Descriptor{ |
||||||
|
MediaType: mediaType, |
||||||
|
Size: int64(len(p)), |
||||||
|
Digest: dgstr.Digest(), |
||||||
|
} |
||||||
|
|
||||||
|
return writer.Commit(ctx, desc) |
||||||
|
} |
||||||
|
|
||||||
|
type optionFunc func(interface{}) error |
||||||
|
|
||||||
|
func (f optionFunc) Apply(v interface{}) error { |
||||||
|
return f(v) |
||||||
|
} |
||||||
|
|
||||||
|
// WithMountFrom returns a BlobCreateOption which designates that the blob should be
|
||||||
|
// mounted from the given canonical reference.
|
||||||
|
func WithMountFrom(ref reference.Canonical) distribution.BlobCreateOption { |
||||||
|
return optionFunc(func(v interface{}) error { |
||||||
|
opts, ok := v.(*distribution.CreateOptions) |
||||||
|
if !ok { |
||||||
|
return fmt.Errorf("unexpected options type: %T", v) |
||||||
|
} |
||||||
|
|
||||||
|
opts.Mount.ShouldMount = true |
||||||
|
opts.Mount.From = ref |
||||||
|
|
||||||
|
return nil |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func (bs *blobs) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) { |
||||||
|
var opts distribution.CreateOptions |
||||||
|
|
||||||
|
for _, option := range options { |
||||||
|
err := option.Apply(&opts) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var values []url.Values |
||||||
|
|
||||||
|
if opts.Mount.ShouldMount { |
||||||
|
values = append(values, url.Values{"from": {opts.Mount.From.Name()}, "mount": {opts.Mount.From.Digest().String()}}) |
||||||
|
} |
||||||
|
|
||||||
|
u, err := bs.ub.BuildBlobUploadURL(bs.name, values...) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
resp, err := bs.client.Post(u, "", nil) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
switch resp.StatusCode { |
||||||
|
case http.StatusCreated: |
||||||
|
desc, err := bs.statter.Stat(ctx, opts.Mount.From.Digest()) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return nil, distribution.ErrBlobMounted{From: opts.Mount.From, Descriptor: desc} |
||||||
|
case http.StatusAccepted: |
||||||
|
// TODO(dmcgowan): Check for invalid UUID
|
||||||
|
uuid := resp.Header.Get("Docker-Upload-UUID") |
||||||
|
location, err := sanitizeLocation(resp.Header.Get("Location"), u) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return &httpBlobUpload{ |
||||||
|
statter: bs.statter, |
||||||
|
client: bs.client, |
||||||
|
uuid: uuid, |
||||||
|
startedAt: time.Now(), |
||||||
|
location: location, |
||||||
|
}, nil |
||||||
|
default: |
||||||
|
return nil, HandleErrorResponse(resp) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (bs *blobs) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) { |
||||||
|
panic("not implemented") |
||||||
|
} |
||||||
|
|
||||||
|
func (bs *blobs) Delete(ctx context.Context, dgst digest.Digest) error { |
||||||
|
return bs.statter.Clear(ctx, dgst) |
||||||
|
} |
||||||
|
|
||||||
|
type blobStatter struct { |
||||||
|
name reference.Named |
||||||
|
ub *v2.URLBuilder |
||||||
|
client *http.Client |
||||||
|
} |
||||||
|
|
||||||
|
func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { |
||||||
|
ref, err := reference.WithDigest(bs.name, dgst) |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
u, err := bs.ub.BuildBlobURL(ref) |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
|
||||||
|
resp, err := bs.client.Head(u) |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
if SuccessStatus(resp.StatusCode) { |
||||||
|
lengthHeader := resp.Header.Get("Content-Length") |
||||||
|
if lengthHeader == "" { |
||||||
|
return distribution.Descriptor{}, fmt.Errorf("missing content-length header for request: %s", u) |
||||||
|
} |
||||||
|
|
||||||
|
length, err := strconv.ParseInt(lengthHeader, 10, 64) |
||||||
|
if err != nil { |
||||||
|
return distribution.Descriptor{}, fmt.Errorf("error parsing content-length: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
return distribution.Descriptor{ |
||||||
|
MediaType: resp.Header.Get("Content-Type"), |
||||||
|
Size: length, |
||||||
|
Digest: dgst, |
||||||
|
}, nil |
||||||
|
} else if resp.StatusCode == http.StatusNotFound { |
||||||
|
return distribution.Descriptor{}, distribution.ErrBlobUnknown |
||||||
|
} |
||||||
|
return distribution.Descriptor{}, HandleErrorResponse(resp) |
||||||
|
} |
||||||
|
|
||||||
|
func buildCatalogValues(maxEntries int, last string) url.Values { |
||||||
|
values := url.Values{} |
||||||
|
|
||||||
|
if maxEntries > 0 { |
||||||
|
values.Add("n", strconv.Itoa(maxEntries)) |
||||||
|
} |
||||||
|
|
||||||
|
if last != "" { |
||||||
|
values.Add("last", last) |
||||||
|
} |
||||||
|
|
||||||
|
return values |
||||||
|
} |
||||||
|
|
||||||
|
func (bs *blobStatter) Clear(ctx context.Context, dgst digest.Digest) error { |
||||||
|
ref, err := reference.WithDigest(bs.name, dgst) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
blobURL, err := bs.ub.BuildBlobURL(ref) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
req, err := http.NewRequest("DELETE", blobURL, nil) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
resp, err := bs.client.Do(req) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
if SuccessStatus(resp.StatusCode) { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return HandleErrorResponse(resp) |
||||||
|
} |
||||||
|
|
||||||
|
func (bs *blobStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { |
||||||
|
return nil |
||||||
|
} |
250
vendor/github.com/docker/distribution/registry/client/transport/http_reader.go
generated
vendored
250
vendor/github.com/docker/distribution/registry/client/transport/http_reader.go
generated
vendored
@ -0,0 +1,250 @@ |
|||||||
|
package transport |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
"regexp" |
||||||
|
"strconv" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
contentRangeRegexp = regexp.MustCompile(`bytes ([0-9]+)-([0-9]+)/([0-9]+|\\*)`) |
||||||
|
|
||||||
|
// ErrWrongCodeForByteRange is returned if the client sends a request
|
||||||
|
// with a Range header but the server returns a 2xx or 3xx code other
|
||||||
|
// than 206 Partial Content.
|
||||||
|
ErrWrongCodeForByteRange = errors.New("expected HTTP 206 from byte range request") |
||||||
|
) |
||||||
|
|
||||||
|
// ReadSeekCloser combines io.ReadSeeker with io.Closer.
|
||||||
|
type ReadSeekCloser interface { |
||||||
|
io.ReadSeeker |
||||||
|
io.Closer |
||||||
|
} |
||||||
|
|
||||||
|
// NewHTTPReadSeeker handles reading from an HTTP endpoint using a GET
|
||||||
|
// request. When seeking and starting a read from a non-zero offset
|
||||||
|
// the a "Range" header will be added which sets the offset.
|
||||||
|
// TODO(dmcgowan): Move this into a separate utility package
|
||||||
|
func NewHTTPReadSeeker(client *http.Client, url string, errorHandler func(*http.Response) error) ReadSeekCloser { |
||||||
|
return &httpReadSeeker{ |
||||||
|
client: client, |
||||||
|
url: url, |
||||||
|
errorHandler: errorHandler, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type httpReadSeeker struct { |
||||||
|
client *http.Client |
||||||
|
url string |
||||||
|
|
||||||
|
// errorHandler creates an error from an unsuccessful HTTP response.
|
||||||
|
// This allows the error to be created with the HTTP response body
|
||||||
|
// without leaking the body through a returned error.
|
||||||
|
errorHandler func(*http.Response) error |
||||||
|
|
||||||
|
size int64 |
||||||
|
|
||||||
|
// rc is the remote read closer.
|
||||||
|
rc io.ReadCloser |
||||||
|
// readerOffset tracks the offset as of the last read.
|
||||||
|
readerOffset int64 |
||||||
|
// seekOffset allows Seek to override the offset. Seek changes
|
||||||
|
// seekOffset instead of changing readOffset directly so that
|
||||||
|
// connection resets can be delayed and possibly avoided if the
|
||||||
|
// seek is undone (i.e. seeking to the end and then back to the
|
||||||
|
// beginning).
|
||||||
|
seekOffset int64 |
||||||
|
err error |
||||||
|
} |
||||||
|
|
||||||
|
func (hrs *httpReadSeeker) Read(p []byte) (n int, err error) { |
||||||
|
if hrs.err != nil { |
||||||
|
return 0, hrs.err |
||||||
|
} |
||||||
|
|
||||||
|
// If we sought to a different position, we need to reset the
|
||||||
|
// connection. This logic is here instead of Seek so that if
|
||||||
|
// a seek is undone before the next read, the connection doesn't
|
||||||
|
// need to be closed and reopened. A common example of this is
|
||||||
|
// seeking to the end to determine the length, and then seeking
|
||||||
|
// back to the original position.
|
||||||
|
if hrs.readerOffset != hrs.seekOffset { |
||||||
|
hrs.reset() |
||||||
|
} |
||||||
|
|
||||||
|
hrs.readerOffset = hrs.seekOffset |
||||||
|
|
||||||
|
rd, err := hrs.reader() |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
|
||||||
|
n, err = rd.Read(p) |
||||||
|
hrs.seekOffset += int64(n) |
||||||
|
hrs.readerOffset += int64(n) |
||||||
|
|
||||||
|
return n, err |
||||||
|
} |
||||||
|
|
||||||
|
func (hrs *httpReadSeeker) Seek(offset int64, whence int) (int64, error) { |
||||||
|
if hrs.err != nil { |
||||||
|
return 0, hrs.err |
||||||
|
} |
||||||
|
|
||||||
|
lastReaderOffset := hrs.readerOffset |
||||||
|
|
||||||
|
if whence == io.SeekStart && hrs.rc == nil { |
||||||
|
// If no request has been made yet, and we are seeking to an
|
||||||
|
// absolute position, set the read offset as well to avoid an
|
||||||
|
// unnecessary request.
|
||||||
|
hrs.readerOffset = offset |
||||||
|
} |
||||||
|
|
||||||
|
_, err := hrs.reader() |
||||||
|
if err != nil { |
||||||
|
hrs.readerOffset = lastReaderOffset |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
|
||||||
|
newOffset := hrs.seekOffset |
||||||
|
|
||||||
|
switch whence { |
||||||
|
case io.SeekCurrent: |
||||||
|
newOffset += offset |
||||||
|
case io.SeekEnd: |
||||||
|
if hrs.size < 0 { |
||||||
|
return 0, errors.New("content length not known") |
||||||
|
} |
||||||
|
newOffset = hrs.size + offset |
||||||
|
case io.SeekStart: |
||||||
|
newOffset = offset |
||||||
|
} |
||||||
|
|
||||||
|
if newOffset < 0 { |
||||||
|
err = errors.New("cannot seek to negative position") |
||||||
|
} else { |
||||||
|
hrs.seekOffset = newOffset |
||||||
|
} |
||||||
|
|
||||||
|
return hrs.seekOffset, err |
||||||
|
} |
||||||
|
|
||||||
|
func (hrs *httpReadSeeker) Close() error { |
||||||
|
if hrs.err != nil { |
||||||
|
return hrs.err |
||||||
|
} |
||||||
|
|
||||||
|
// close and release reader chain
|
||||||
|
if hrs.rc != nil { |
||||||
|
hrs.rc.Close() |
||||||
|
} |
||||||
|
|
||||||
|
hrs.rc = nil |
||||||
|
|
||||||
|
hrs.err = errors.New("httpLayer: closed") |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (hrs *httpReadSeeker) reset() { |
||||||
|
if hrs.err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
if hrs.rc != nil { |
||||||
|
hrs.rc.Close() |
||||||
|
hrs.rc = nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (hrs *httpReadSeeker) reader() (io.Reader, error) { |
||||||
|
if hrs.err != nil { |
||||||
|
return nil, hrs.err |
||||||
|
} |
||||||
|
|
||||||
|
if hrs.rc != nil { |
||||||
|
return hrs.rc, nil |
||||||
|
} |
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", hrs.url, nil) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if hrs.readerOffset > 0 { |
||||||
|
// If we are at different offset, issue a range request from there.
|
||||||
|
req.Header.Add("Range", fmt.Sprintf("bytes=%d-", hrs.readerOffset)) |
||||||
|
// TODO: get context in here
|
||||||
|
// context.GetLogger(hrs.context).Infof("Range: %s", req.Header.Get("Range"))
|
||||||
|
} |
||||||
|
|
||||||
|
req.Header.Add("Accept-Encoding", "identity") |
||||||
|
resp, err := hrs.client.Do(req) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Normally would use client.SuccessStatus, but that would be a cyclic
|
||||||
|
// import
|
||||||
|
if resp.StatusCode >= 200 && resp.StatusCode <= 399 { |
||||||
|
if hrs.readerOffset > 0 { |
||||||
|
if resp.StatusCode != http.StatusPartialContent { |
||||||
|
return nil, ErrWrongCodeForByteRange |
||||||
|
} |
||||||
|
|
||||||
|
contentRange := resp.Header.Get("Content-Range") |
||||||
|
if contentRange == "" { |
||||||
|
return nil, errors.New("no Content-Range header found in HTTP 206 response") |
||||||
|
} |
||||||
|
|
||||||
|
submatches := contentRangeRegexp.FindStringSubmatch(contentRange) |
||||||
|
if len(submatches) < 4 { |
||||||
|
return nil, fmt.Errorf("could not parse Content-Range header: %s", contentRange) |
||||||
|
} |
||||||
|
|
||||||
|
startByte, err := strconv.ParseUint(submatches[1], 10, 64) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("could not parse start of range in Content-Range header: %s", contentRange) |
||||||
|
} |
||||||
|
|
||||||
|
if startByte != uint64(hrs.readerOffset) { |
||||||
|
return nil, fmt.Errorf("received Content-Range starting at offset %d instead of requested %d", startByte, hrs.readerOffset) |
||||||
|
} |
||||||
|
|
||||||
|
endByte, err := strconv.ParseUint(submatches[2], 10, 64) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("could not parse end of range in Content-Range header: %s", contentRange) |
||||||
|
} |
||||||
|
|
||||||
|
if submatches[3] == "*" { |
||||||
|
hrs.size = -1 |
||||||
|
} else { |
||||||
|
size, err := strconv.ParseUint(submatches[3], 10, 64) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("could not parse total size in Content-Range header: %s", contentRange) |
||||||
|
} |
||||||
|
|
||||||
|
if endByte+1 != size { |
||||||
|
return nil, fmt.Errorf("range in Content-Range stops before the end of the content: %s", contentRange) |
||||||
|
} |
||||||
|
|
||||||
|
hrs.size = int64(size) |
||||||
|
} |
||||||
|
} else if resp.StatusCode == http.StatusOK { |
||||||
|
hrs.size = resp.ContentLength |
||||||
|
} else { |
||||||
|
hrs.size = -1 |
||||||
|
} |
||||||
|
hrs.rc = resp.Body |
||||||
|
} else { |
||||||
|
defer resp.Body.Close() |
||||||
|
if hrs.errorHandler != nil { |
||||||
|
return nil, hrs.errorHandler(resp) |
||||||
|
} |
||||||
|
return nil, fmt.Errorf("unexpected status resolving reader: %v", resp.Status) |
||||||
|
} |
||||||
|
|
||||||
|
return hrs.rc, nil |
||||||
|
} |
@ -0,0 +1,147 @@ |
|||||||
|
package transport |
||||||
|
|
||||||
|
import ( |
||||||
|
"io" |
||||||
|
"net/http" |
||||||
|
"sync" |
||||||
|
) |
||||||
|
|
||||||
|
// RequestModifier represents an object which will do an inplace
|
||||||
|
// modification of an HTTP request.
|
||||||
|
type RequestModifier interface { |
||||||
|
ModifyRequest(*http.Request) error |
||||||
|
} |
||||||
|
|
||||||
|
type headerModifier http.Header |
||||||
|
|
||||||
|
// NewHeaderRequestModifier returns a new RequestModifier which will
|
||||||
|
// add the given headers to a request.
|
||||||
|
func NewHeaderRequestModifier(header http.Header) RequestModifier { |
||||||
|
return headerModifier(header) |
||||||
|
} |
||||||
|
|
||||||
|
func (h headerModifier) ModifyRequest(req *http.Request) error { |
||||||
|
for k, s := range http.Header(h) { |
||||||
|
req.Header[k] = append(req.Header[k], s...) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// NewTransport creates a new transport which will apply modifiers to
|
||||||
|
// the request on a RoundTrip call.
|
||||||
|
func NewTransport(base http.RoundTripper, modifiers ...RequestModifier) http.RoundTripper { |
||||||
|
return &transport{ |
||||||
|
Modifiers: modifiers, |
||||||
|
Base: base, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// transport is an http.RoundTripper that makes HTTP requests after
|
||||||
|
// copying and modifying the request
|
||||||
|
type transport struct { |
||||||
|
Modifiers []RequestModifier |
||||||
|
Base http.RoundTripper |
||||||
|
|
||||||
|
mu sync.Mutex // guards modReq
|
||||||
|
modReq map[*http.Request]*http.Request // original -> modified
|
||||||
|
} |
||||||
|
|
||||||
|
// RoundTrip authorizes and authenticates the request with an
|
||||||
|
// access token. If no token exists or token is expired,
|
||||||
|
// tries to refresh/fetch a new token.
|
||||||
|
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { |
||||||
|
req2 := cloneRequest(req) |
||||||
|
for _, modifier := range t.Modifiers { |
||||||
|
if err := modifier.ModifyRequest(req2); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
t.setModReq(req, req2) |
||||||
|
res, err := t.base().RoundTrip(req2) |
||||||
|
if err != nil { |
||||||
|
t.setModReq(req, nil) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
res.Body = &onEOFReader{ |
||||||
|
rc: res.Body, |
||||||
|
fn: func() { t.setModReq(req, nil) }, |
||||||
|
} |
||||||
|
return res, nil |
||||||
|
} |
||||||
|
|
||||||
|
// CancelRequest cancels an in-flight request by closing its connection.
|
||||||
|
func (t *transport) CancelRequest(req *http.Request) { |
||||||
|
type canceler interface { |
||||||
|
CancelRequest(*http.Request) |
||||||
|
} |
||||||
|
if cr, ok := t.base().(canceler); ok { |
||||||
|
t.mu.Lock() |
||||||
|
modReq := t.modReq[req] |
||||||
|
delete(t.modReq, req) |
||||||
|
t.mu.Unlock() |
||||||
|
cr.CancelRequest(modReq) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (t *transport) base() http.RoundTripper { |
||||||
|
if t.Base != nil { |
||||||
|
return t.Base |
||||||
|
} |
||||||
|
return http.DefaultTransport |
||||||
|
} |
||||||
|
|
||||||
|
func (t *transport) setModReq(orig, mod *http.Request) { |
||||||
|
t.mu.Lock() |
||||||
|
defer t.mu.Unlock() |
||||||
|
if t.modReq == nil { |
||||||
|
t.modReq = make(map[*http.Request]*http.Request) |
||||||
|
} |
||||||
|
if mod == nil { |
||||||
|
delete(t.modReq, orig) |
||||||
|
} else { |
||||||
|
t.modReq[orig] = mod |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// cloneRequest returns a clone of the provided *http.Request.
|
||||||
|
// The clone is a shallow copy of the struct and its Header map.
|
||||||
|
func cloneRequest(r *http.Request) *http.Request { |
||||||
|
// shallow copy of the struct
|
||||||
|
r2 := new(http.Request) |
||||||
|
*r2 = *r |
||||||
|
// deep copy of the Header
|
||||||
|
r2.Header = make(http.Header, len(r.Header)) |
||||||
|
for k, s := range r.Header { |
||||||
|
r2.Header[k] = append([]string(nil), s...) |
||||||
|
} |
||||||
|
|
||||||
|
return r2 |
||||||
|
} |
||||||
|
|
||||||
|
type onEOFReader struct { |
||||||
|
rc io.ReadCloser |
||||||
|
fn func() |
||||||
|
} |
||||||
|
|
||||||
|
func (r *onEOFReader) Read(p []byte) (n int, err error) { |
||||||
|
n, err = r.rc.Read(p) |
||||||
|
if err == io.EOF { |
||||||
|
r.runFunc() |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (r *onEOFReader) Close() error { |
||||||
|
err := r.rc.Close() |
||||||
|
r.runFunc() |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func (r *onEOFReader) runFunc() { |
||||||
|
if fn := r.fn; fn != nil { |
||||||
|
fn() |
||||||
|
r.fn = nil |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
// Package cache provides facilities to speed up access to the storage
|
||||||
|
// backend.
|
||||||
|
package cache |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"github.com/docker/distribution" |
||||||
|
) |
||||||
|
|
||||||
|
// BlobDescriptorCacheProvider provides repository scoped
|
||||||
|
// BlobDescriptorService cache instances and a global descriptor cache.
|
||||||
|
type BlobDescriptorCacheProvider interface { |
||||||
|
distribution.BlobDescriptorService |
||||||
|
|
||||||
|
RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) |
||||||
|
} |
||||||
|
|
||||||
|
// ValidateDescriptor provides a helper function to ensure that caches have
|
||||||
|
// common criteria for admitting descriptors.
|
||||||
|
func ValidateDescriptor(desc distribution.Descriptor) error { |
||||||
|
if err := desc.Digest.Validate(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if desc.Size < 0 { |
||||||
|
return fmt.Errorf("cache: invalid length in descriptor: %v < 0", desc.Size) |
||||||
|
} |
||||||
|
|
||||||
|
if desc.MediaType == "" { |
||||||
|
return fmt.Errorf("cache: empty mediatype on descriptor: %v", desc) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
129
vendor/github.com/docker/distribution/registry/storage/cache/cachedblobdescriptorstore.go
generated
vendored
129
vendor/github.com/docker/distribution/registry/storage/cache/cachedblobdescriptorstore.go
generated
vendored
@ -0,0 +1,129 @@ |
|||||||
|
package cache |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
|
||||||
|
"github.com/docker/distribution" |
||||||
|
prometheus "github.com/docker/distribution/metrics" |
||||||
|
"github.com/opencontainers/go-digest" |
||||||
|
) |
||||||
|
|
||||||
|
// Metrics is used to hold metric counters
|
||||||
|
// related to the number of times a cache was
|
||||||
|
// hit or missed.
|
||||||
|
type Metrics struct { |
||||||
|
Requests uint64 |
||||||
|
Hits uint64 |
||||||
|
Misses uint64 |
||||||
|
} |
||||||
|
|
||||||
|
// Logger can be provided on the MetricsTracker to log errors.
|
||||||
|
//
|
||||||
|
// Usually, this is just a proxy to dcontext.GetLogger.
|
||||||
|
type Logger interface { |
||||||
|
Errorf(format string, args ...interface{}) |
||||||
|
} |
||||||
|
|
||||||
|
// MetricsTracker represents a metric tracker
|
||||||
|
// which simply counts the number of hits and misses.
|
||||||
|
type MetricsTracker interface { |
||||||
|
Hit() |
||||||
|
Miss() |
||||||
|
Metrics() Metrics |
||||||
|
Logger(context.Context) Logger |
||||||
|
} |
||||||
|
|
||||||
|
type cachedBlobStatter struct { |
||||||
|
cache distribution.BlobDescriptorService |
||||||
|
backend distribution.BlobDescriptorService |
||||||
|
tracker MetricsTracker |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
// cacheCount is the number of total cache request received/hits/misses
|
||||||
|
cacheCount = prometheus.StorageNamespace.NewLabeledCounter("cache", "The number of cache request received", "type") |
||||||
|
) |
||||||
|
|
||||||
|
// NewCachedBlobStatter creates a new statter which prefers a cache and
|
||||||
|
// falls back to a backend.
|
||||||
|
func NewCachedBlobStatter(cache distribution.BlobDescriptorService, backend distribution.BlobDescriptorService) distribution.BlobDescriptorService { |
||||||
|
return &cachedBlobStatter{ |
||||||
|
cache: cache, |
||||||
|
backend: backend, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// NewCachedBlobStatterWithMetrics creates a new statter which prefers a cache and
|
||||||
|
// falls back to a backend. Hits and misses will send to the tracker.
|
||||||
|
func NewCachedBlobStatterWithMetrics(cache distribution.BlobDescriptorService, backend distribution.BlobDescriptorService, tracker MetricsTracker) distribution.BlobStatter { |
||||||
|
return &cachedBlobStatter{ |
||||||
|
cache: cache, |
||||||
|
backend: backend, |
||||||
|
tracker: tracker, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (cbds *cachedBlobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { |
||||||
|
cacheCount.WithValues("Request").Inc(1) |
||||||
|
desc, err := cbds.cache.Stat(ctx, dgst) |
||||||
|
if err != nil { |
||||||
|
if err != distribution.ErrBlobUnknown { |
||||||
|
logErrorf(ctx, cbds.tracker, "error retrieving descriptor from cache: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
goto fallback |
||||||
|
} |
||||||
|
cacheCount.WithValues("Hit").Inc(1) |
||||||
|
if cbds.tracker != nil { |
||||||
|
cbds.tracker.Hit() |
||||||
|
} |
||||||
|
return desc, nil |
||||||
|
fallback: |
||||||
|
cacheCount.WithValues("Miss").Inc(1) |
||||||
|
if cbds.tracker != nil { |
||||||
|
cbds.tracker.Miss() |
||||||
|
} |
||||||
|
desc, err = cbds.backend.Stat(ctx, dgst) |
||||||
|
if err != nil { |
||||||
|
return desc, err |
||||||
|
} |
||||||
|
|
||||||
|
if err := cbds.cache.SetDescriptor(ctx, dgst, desc); err != nil { |
||||||
|
logErrorf(ctx, cbds.tracker, "error adding descriptor %v to cache: %v", desc.Digest, err) |
||||||
|
} |
||||||
|
|
||||||
|
return desc, err |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
func (cbds *cachedBlobStatter) Clear(ctx context.Context, dgst digest.Digest) error { |
||||||
|
err := cbds.cache.Clear(ctx, dgst) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
err = cbds.backend.Clear(ctx, dgst) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (cbds *cachedBlobStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { |
||||||
|
if err := cbds.cache.SetDescriptor(ctx, dgst, desc); err != nil { |
||||||
|
logErrorf(ctx, cbds.tracker, "error adding descriptor %v to cache: %v", desc.Digest, err) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func logErrorf(ctx context.Context, tracker MetricsTracker, format string, args ...interface{}) { |
||||||
|
if tracker == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
logger := tracker.Logger(ctx) |
||||||
|
if logger == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
logger.Errorf(format, args...) |
||||||
|
} |
179
vendor/github.com/docker/distribution/registry/storage/cache/memory/memory.go
generated
vendored
179
vendor/github.com/docker/distribution/registry/storage/cache/memory/memory.go
generated
vendored
@ -0,0 +1,179 @@ |
|||||||
|
package memory |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/docker/distribution" |
||||||
|
"github.com/docker/distribution/reference" |
||||||
|
"github.com/docker/distribution/registry/storage/cache" |
||||||
|
"github.com/opencontainers/go-digest" |
||||||
|
) |
||||||
|
|
||||||
|
type inMemoryBlobDescriptorCacheProvider struct { |
||||||
|
global *mapBlobDescriptorCache |
||||||
|
repositories map[string]*mapBlobDescriptorCache |
||||||
|
mu sync.RWMutex |
||||||
|
} |
||||||
|
|
||||||
|
// NewInMemoryBlobDescriptorCacheProvider returns a new mapped-based cache for
|
||||||
|
// storing blob descriptor data.
|
||||||
|
func NewInMemoryBlobDescriptorCacheProvider() cache.BlobDescriptorCacheProvider { |
||||||
|
return &inMemoryBlobDescriptorCacheProvider{ |
||||||
|
global: newMapBlobDescriptorCache(), |
||||||
|
repositories: make(map[string]*mapBlobDescriptorCache), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) { |
||||||
|
if _, err := reference.ParseNormalizedNamed(repo); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
imbdcp.mu.RLock() |
||||||
|
defer imbdcp.mu.RUnlock() |
||||||
|
|
||||||
|
return &repositoryScopedInMemoryBlobDescriptorCache{ |
||||||
|
repo: repo, |
||||||
|
parent: imbdcp, |
||||||
|
repository: imbdcp.repositories[repo], |
||||||
|
}, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (imbdcp *inMemoryBlobDescriptorCacheProvider) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { |
||||||
|
return imbdcp.global.Stat(ctx, dgst) |
||||||
|
} |
||||||
|
|
||||||
|
func (imbdcp *inMemoryBlobDescriptorCacheProvider) Clear(ctx context.Context, dgst digest.Digest) error { |
||||||
|
return imbdcp.global.Clear(ctx, dgst) |
||||||
|
} |
||||||
|
|
||||||
|
func (imbdcp *inMemoryBlobDescriptorCacheProvider) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { |
||||||
|
_, err := imbdcp.Stat(ctx, dgst) |
||||||
|
if err == distribution.ErrBlobUnknown { |
||||||
|
|
||||||
|
if dgst.Algorithm() != desc.Digest.Algorithm() && dgst != desc.Digest { |
||||||
|
// if the digests differ, set the other canonical mapping
|
||||||
|
if err := imbdcp.global.SetDescriptor(ctx, desc.Digest, desc); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// unknown, just set it
|
||||||
|
return imbdcp.global.SetDescriptor(ctx, dgst, desc) |
||||||
|
} |
||||||
|
|
||||||
|
// we already know it, do nothing
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// repositoryScopedInMemoryBlobDescriptorCache provides the request scoped
|
||||||
|
// repository cache. Instances are not thread-safe but the delegated
|
||||||
|
// operations are.
|
||||||
|
type repositoryScopedInMemoryBlobDescriptorCache struct { |
||||||
|
repo string |
||||||
|
parent *inMemoryBlobDescriptorCacheProvider // allows lazy allocation of repo's map
|
||||||
|
repository *mapBlobDescriptorCache |
||||||
|
} |
||||||
|
|
||||||
|
func (rsimbdcp *repositoryScopedInMemoryBlobDescriptorCache) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { |
||||||
|
rsimbdcp.parent.mu.Lock() |
||||||
|
repo := rsimbdcp.repository |
||||||
|
rsimbdcp.parent.mu.Unlock() |
||||||
|
|
||||||
|
if repo == nil { |
||||||
|
return distribution.Descriptor{}, distribution.ErrBlobUnknown |
||||||
|
} |
||||||
|
|
||||||
|
return repo.Stat(ctx, dgst) |
||||||
|
} |
||||||
|
|
||||||
|
func (rsimbdcp *repositoryScopedInMemoryBlobDescriptorCache) Clear(ctx context.Context, dgst digest.Digest) error { |
||||||
|
rsimbdcp.parent.mu.Lock() |
||||||
|
repo := rsimbdcp.repository |
||||||
|
rsimbdcp.parent.mu.Unlock() |
||||||
|
|
||||||
|
if repo == nil { |
||||||
|
return distribution.ErrBlobUnknown |
||||||
|
} |
||||||
|
|
||||||
|
return repo.Clear(ctx, dgst) |
||||||
|
} |
||||||
|
|
||||||
|
func (rsimbdcp *repositoryScopedInMemoryBlobDescriptorCache) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { |
||||||
|
rsimbdcp.parent.mu.Lock() |
||||||
|
repo := rsimbdcp.repository |
||||||
|
if repo == nil { |
||||||
|
// allocate map since we are setting it now.
|
||||||
|
var ok bool |
||||||
|
// have to read back value since we may have allocated elsewhere.
|
||||||
|
repo, ok = rsimbdcp.parent.repositories[rsimbdcp.repo] |
||||||
|
if !ok { |
||||||
|
repo = newMapBlobDescriptorCache() |
||||||
|
rsimbdcp.parent.repositories[rsimbdcp.repo] = repo |
||||||
|
} |
||||||
|
rsimbdcp.repository = repo |
||||||
|
} |
||||||
|
rsimbdcp.parent.mu.Unlock() |
||||||
|
|
||||||
|
if err := repo.SetDescriptor(ctx, dgst, desc); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return rsimbdcp.parent.SetDescriptor(ctx, dgst, desc) |
||||||
|
} |
||||||
|
|
||||||
|
// mapBlobDescriptorCache provides a simple map-based implementation of the
|
||||||
|
// descriptor cache.
|
||||||
|
type mapBlobDescriptorCache struct { |
||||||
|
descriptors map[digest.Digest]distribution.Descriptor |
||||||
|
mu sync.RWMutex |
||||||
|
} |
||||||
|
|
||||||
|
var _ distribution.BlobDescriptorService = &mapBlobDescriptorCache{} |
||||||
|
|
||||||
|
func newMapBlobDescriptorCache() *mapBlobDescriptorCache { |
||||||
|
return &mapBlobDescriptorCache{ |
||||||
|
descriptors: make(map[digest.Digest]distribution.Descriptor), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (mbdc *mapBlobDescriptorCache) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { |
||||||
|
if err := dgst.Validate(); err != nil { |
||||||
|
return distribution.Descriptor{}, err |
||||||
|
} |
||||||
|
|
||||||
|
mbdc.mu.RLock() |
||||||
|
defer mbdc.mu.RUnlock() |
||||||
|
|
||||||
|
desc, ok := mbdc.descriptors[dgst] |
||||||
|
if !ok { |
||||||
|
return distribution.Descriptor{}, distribution.ErrBlobUnknown |
||||||
|
} |
||||||
|
|
||||||
|
return desc, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (mbdc *mapBlobDescriptorCache) Clear(ctx context.Context, dgst digest.Digest) error { |
||||||
|
mbdc.mu.Lock() |
||||||
|
defer mbdc.mu.Unlock() |
||||||
|
|
||||||
|
delete(mbdc.descriptors, dgst) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (mbdc *mapBlobDescriptorCache) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { |
||||||
|
if err := dgst.Validate(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if err := cache.ValidateDescriptor(desc); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
mbdc.mu.Lock() |
||||||
|
defer mbdc.mu.Unlock() |
||||||
|
|
||||||
|
mbdc.descriptors[dgst] = desc |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,126 @@ |
|||||||
|
// Package uuid provides simple UUID generation. Only version 4 style UUIDs
|
||||||
|
// can be generated.
|
||||||
|
//
|
||||||
|
// Please see http://tools.ietf.org/html/rfc4122 for details on UUIDs.
|
||||||
|
package uuid |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/rand" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"syscall" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
// Bits is the number of bits in a UUID
|
||||||
|
Bits = 128 |
||||||
|
|
||||||
|
// Size is the number of bytes in a UUID
|
||||||
|
Size = Bits / 8 |
||||||
|
|
||||||
|
format = "%08x-%04x-%04x-%04x-%012x" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
// ErrUUIDInvalid indicates a parsed string is not a valid uuid.
|
||||||
|
ErrUUIDInvalid = fmt.Errorf("invalid uuid") |
||||||
|
|
||||||
|
// Loggerf can be used to override the default logging destination. Such
|
||||||
|
// log messages in this library should be logged at warning or higher.
|
||||||
|
Loggerf = func(format string, args ...interface{}) {} |
||||||
|
) |
||||||
|
|
||||||
|
// UUID represents a UUID value. UUIDs can be compared and set to other values
|
||||||
|
// and accessed by byte.
|
||||||
|
type UUID [Size]byte |
||||||
|
|
||||||
|
// Generate creates a new, version 4 uuid.
|
||||||
|
func Generate() (u UUID) { |
||||||
|
const ( |
||||||
|
// ensures we backoff for less than 450ms total. Use the following to
|
||||||
|
// select new value, in units of 10ms:
|
||||||
|
// n*(n+1)/2 = d -> n^2 + n - 2d -> n = (sqrt(8d + 1) - 1)/2
|
||||||
|
maxretries = 9 |
||||||
|
backoff = time.Millisecond * 10 |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
totalBackoff time.Duration |
||||||
|
count int |
||||||
|
retries int |
||||||
|
) |
||||||
|
|
||||||
|
for { |
||||||
|
// This should never block but the read may fail. Because of this,
|
||||||
|
// we just try to read the random number generator until we get
|
||||||
|
// something. This is a very rare condition but may happen.
|
||||||
|
b := time.Duration(retries) * backoff |
||||||
|
time.Sleep(b) |
||||||
|
totalBackoff += b |
||||||
|
|
||||||
|
n, err := io.ReadFull(rand.Reader, u[count:]) |
||||||
|
if err != nil { |
||||||
|
if retryOnError(err) && retries < maxretries { |
||||||
|
count += n |
||||||
|
retries++ |
||||||
|
Loggerf("error generating version 4 uuid, retrying: %v", err) |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// Any other errors represent a system problem. What did someone
|
||||||
|
// do to /dev/urandom?
|
||||||
|
panic(fmt.Errorf("error reading random number generator, retried for %v: %v", totalBackoff.String(), err)) |
||||||
|
} |
||||||
|
|
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
u[6] = (u[6] & 0x0f) | 0x40 // set version byte
|
||||||
|
u[8] = (u[8] & 0x3f) | 0x80 // set high order byte 0b10{8,9,a,b}
|
||||||
|
|
||||||
|
return u |
||||||
|
} |
||||||
|
|
||||||
|
// Parse attempts to extract a uuid from the string or returns an error.
|
||||||
|
func Parse(s string) (u UUID, err error) { |
||||||
|
if len(s) != 36 { |
||||||
|
return UUID{}, ErrUUIDInvalid |
||||||
|
} |
||||||
|
|
||||||
|
// create stack addresses for each section of the uuid.
|
||||||
|
p := make([][]byte, 5) |
||||||
|
|
||||||
|
if _, err := fmt.Sscanf(s, format, &p[0], &p[1], &p[2], &p[3], &p[4]); err != nil { |
||||||
|
return u, err |
||||||
|
} |
||||||
|
|
||||||
|
copy(u[0:4], p[0]) |
||||||
|
copy(u[4:6], p[1]) |
||||||
|
copy(u[6:8], p[2]) |
||||||
|
copy(u[8:10], p[3]) |
||||||
|
copy(u[10:16], p[4]) |
||||||
|
|
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (u UUID) String() string { |
||||||
|
return fmt.Sprintf(format, u[:4], u[4:6], u[6:8], u[8:10], u[10:]) |
||||||
|
} |
||||||
|
|
||||||
|
// retryOnError tries to detect whether or not retrying would be fruitful.
|
||||||
|
func retryOnError(err error) bool { |
||||||
|
switch err := err.(type) { |
||||||
|
case *os.PathError: |
||||||
|
return retryOnError(err.Err) // unpack the target error
|
||||||
|
case syscall.Errno: |
||||||
|
if err == syscall.EPERM { |
||||||
|
// EPERM represents an entropy pool exhaustion, a condition under
|
||||||
|
// which we backoff and retry.
|
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
Copyright (c) 2016 David Calavera |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining |
||||||
|
a copy of this software and associated documentation files (the |
||||||
|
"Software"), to deal in the Software without restriction, including |
||||||
|
without limitation the rights to use, copy, modify, merge, publish, |
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to |
||||||
|
permit persons to whom the Software is furnished to do so, subject to |
||||||
|
the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be |
||||||
|
included in all copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY |
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, |
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE |
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
@ -0,0 +1,121 @@ |
|||||||
|
package client |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/docker/docker-credential-helpers/credentials" |
||||||
|
) |
||||||
|
|
||||||
|
// isValidCredsMessage checks if 'msg' contains invalid credentials error message.
|
||||||
|
// It returns whether the logs are free of invalid credentials errors and the error if it isn't.
|
||||||
|
// error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername.
|
||||||
|
func isValidCredsMessage(msg string) error { |
||||||
|
if credentials.IsCredentialsMissingServerURLMessage(msg) { |
||||||
|
return credentials.NewErrCredentialsMissingServerURL() |
||||||
|
} |
||||||
|
|
||||||
|
if credentials.IsCredentialsMissingUsernameMessage(msg) { |
||||||
|
return credentials.NewErrCredentialsMissingUsername() |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Store uses an external program to save credentials.
|
||||||
|
func Store(program ProgramFunc, creds *credentials.Credentials) error { |
||||||
|
cmd := program("store") |
||||||
|
|
||||||
|
buffer := new(bytes.Buffer) |
||||||
|
if err := json.NewEncoder(buffer).Encode(creds); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
cmd.Input(buffer) |
||||||
|
|
||||||
|
out, err := cmd.Output() |
||||||
|
if err != nil { |
||||||
|
t := strings.TrimSpace(string(out)) |
||||||
|
|
||||||
|
if isValidErr := isValidCredsMessage(t); isValidErr != nil { |
||||||
|
err = isValidErr |
||||||
|
} |
||||||
|
|
||||||
|
return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, t) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Get executes an external program to get the credentials from a native store.
|
||||||
|
func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error) { |
||||||
|
cmd := program("get") |
||||||
|
cmd.Input(strings.NewReader(serverURL)) |
||||||
|
|
||||||
|
out, err := cmd.Output() |
||||||
|
if err != nil { |
||||||
|
t := strings.TrimSpace(string(out)) |
||||||
|
|
||||||
|
if credentials.IsErrCredentialsNotFoundMessage(t) { |
||||||
|
return nil, credentials.NewErrCredentialsNotFound() |
||||||
|
} |
||||||
|
|
||||||
|
if isValidErr := isValidCredsMessage(t); isValidErr != nil { |
||||||
|
err = isValidErr |
||||||
|
} |
||||||
|
|
||||||
|
return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, t) |
||||||
|
} |
||||||
|
|
||||||
|
resp := &credentials.Credentials{ |
||||||
|
ServerURL: serverURL, |
||||||
|
} |
||||||
|
|
||||||
|
if err := json.NewDecoder(bytes.NewReader(out)).Decode(resp); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return resp, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Erase executes a program to remove the server credentials from the native store.
|
||||||
|
func Erase(program ProgramFunc, serverURL string) error { |
||||||
|
cmd := program("erase") |
||||||
|
cmd.Input(strings.NewReader(serverURL)) |
||||||
|
out, err := cmd.Output() |
||||||
|
if err != nil { |
||||||
|
t := strings.TrimSpace(string(out)) |
||||||
|
|
||||||
|
if isValidErr := isValidCredsMessage(t); isValidErr != nil { |
||||||
|
err = isValidErr |
||||||
|
} |
||||||
|
|
||||||
|
return fmt.Errorf("error erasing credentials - err: %v, out: `%s`", err, t) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// List executes a program to list server credentials in the native store.
|
||||||
|
func List(program ProgramFunc) (map[string]string, error) { |
||||||
|
cmd := program("list") |
||||||
|
cmd.Input(strings.NewReader("unused")) |
||||||
|
out, err := cmd.Output() |
||||||
|
if err != nil { |
||||||
|
t := strings.TrimSpace(string(out)) |
||||||
|
|
||||||
|
if isValidErr := isValidCredsMessage(t); isValidErr != nil { |
||||||
|
err = isValidErr |
||||||
|
} |
||||||
|
|
||||||
|
return nil, fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t) |
||||||
|
} |
||||||
|
|
||||||
|
var resp map[string]string |
||||||
|
if err = json.NewDecoder(bytes.NewReader(out)).Decode(&resp); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return resp, nil |
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
package client |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"os/exec" |
||||||
|
) |
||||||
|
|
||||||
|
// Program is an interface to execute external programs.
|
||||||
|
type Program interface { |
||||||
|
Output() ([]byte, error) |
||||||
|
Input(in io.Reader) |
||||||
|
} |
||||||
|
|
||||||
|
// ProgramFunc is a type of function that initializes programs based on arguments.
|
||||||
|
type ProgramFunc func(args ...string) Program |
||||||
|
|
||||||
|
// NewShellProgramFunc creates programs that are executed in a Shell.
|
||||||
|
func NewShellProgramFunc(name string) ProgramFunc { |
||||||
|
return NewShellProgramFuncWithEnv(name, nil) |
||||||
|
} |
||||||
|
|
||||||
|
// NewShellProgramFuncWithEnv creates programs that are executed in a Shell with environment variables
|
||||||
|
func NewShellProgramFuncWithEnv(name string, env *map[string]string) ProgramFunc { |
||||||
|
return func(args ...string) Program { |
||||||
|
return &Shell{cmd: createProgramCmdRedirectErr(name, args, env)} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func createProgramCmdRedirectErr(commandName string, args []string, env *map[string]string) *exec.Cmd { |
||||||
|
programCmd := exec.Command(commandName, args...) |
||||||
|
programCmd.Env = os.Environ() |
||||||
|
if env != nil { |
||||||
|
for k, v := range *env { |
||||||
|
programCmd.Env = append(programCmd.Env, fmt.Sprintf("%s=%s", k, v)) |
||||||
|
} |
||||||
|
} |
||||||
|
programCmd.Stderr = os.Stderr |
||||||
|
return programCmd |
||||||
|
} |
||||||
|
|
||||||
|
// Shell invokes shell commands to talk with a remote credentials helper.
|
||||||
|
type Shell struct { |
||||||
|
cmd *exec.Cmd |
||||||
|
} |
||||||
|
|
||||||
|
// Output returns responses from the remote credentials helper.
|
||||||
|
func (s *Shell) Output() ([]byte, error) { |
||||||
|
return s.cmd.Output() |
||||||
|
} |
||||||
|
|
||||||
|
// Input sets the input to send to a remote credentials helper.
|
||||||
|
func (s *Shell) Input(in io.Reader) { |
||||||
|
s.cmd.Stdin = in |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue