From d8f1d4572bae9926ffbbf84f7b391a43817535ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Jul 2018 00:11:36 +0200 Subject: [PATCH 01/30] Update github.com/urfave/cli MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's probably not strictly necessary, but let's work with the current implementation before worrying about possible idiosyncracies. Signed-off-by: Miloslav Trmač --- vendor.conf | 2 +- vendor/github.com/urfave/cli/LICENSE | 28 +- vendor/github.com/urfave/cli/README.md | 1202 ++++++++++++++--- vendor/github.com/urfave/cli/app.go | 141 +- vendor/github.com/urfave/cli/cli.go | 3 + vendor/github.com/urfave/cli/command.go | 85 +- vendor/github.com/urfave/cli/context.go | 330 ++--- vendor/github.com/urfave/cli/errors.go | 41 +- vendor/github.com/urfave/cli/flag.go | 562 +++++--- .../github.com/urfave/cli/flag_generated.go | 627 +++++++++ vendor/github.com/urfave/cli/help.go | 146 +- 11 files changed, 2345 insertions(+), 822 deletions(-) create mode 100644 vendor/github.com/urfave/cli/flag_generated.go diff --git a/vendor.conf b/vendor.conf index 93052d53..e1faf689 100644 --- a/vendor.conf +++ b/vendor.conf @@ -1,4 +1,4 @@ -github.com/urfave/cli v1.17.0 +github.com/urfave/cli v1.20.0 github.com/kr/pretty v0.1.0 github.com/kr/text v0.1.0 github.com/containers/image master diff --git a/vendor/github.com/urfave/cli/LICENSE b/vendor/github.com/urfave/cli/LICENSE index 5515ccfb..42a597e2 100644 --- a/vendor/github.com/urfave/cli/LICENSE +++ b/vendor/github.com/urfave/cli/LICENSE @@ -1,21 +1,21 @@ -Copyright (C) 2013 Jeremy Saenz -All Rights Reserved. +MIT License -MIT LICENSE +Copyright (c) 2016 Jeremy Saenz & Contributors -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: +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. +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. diff --git a/vendor/github.com/urfave/cli/README.md b/vendor/github.com/urfave/cli/README.md index c1709cef..2bbbd8ea 100644 --- a/vendor/github.com/urfave/cli/README.md +++ b/vendor/github.com/urfave/cli/README.md @@ -1,43 +1,146 @@ -[![Coverage](http://gocover.io/_badge/github.com/codegangsta/cli?0)](http://gocover.io/github.com/codegangsta/cli) -[![Build Status](https://travis-ci.org/codegangsta/cli.svg?branch=master)](https://travis-ci.org/codegangsta/cli) -[![GoDoc](https://godoc.org/github.com/codegangsta/cli?status.svg)](https://godoc.org/github.com/codegangsta/cli) -[![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-codegangsta-cli) -[![Go Report Card](https://goreportcard.com/badge/codegangsta/cli)](https://goreportcard.com/report/codegangsta/cli) +cli +=== -# cli +[![Build Status](https://travis-ci.org/urfave/cli.svg?branch=master)](https://travis-ci.org/urfave/cli) +[![Windows Build Status](https://ci.appveyor.com/api/projects/status/rtgk5xufi932pb2v?svg=true)](https://ci.appveyor.com/project/urfave/cli) +[![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://godoc.org/github.com/urfave/cli) +[![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli) +[![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli) +[![top level coverage](https://gocover.io/_badge/github.com/urfave/cli?0 "top level coverage")](http://gocover.io/github.com/urfave/cli) / +[![altsrc coverage](https://gocover.io/_badge/github.com/urfave/cli/altsrc?0 "altsrc coverage")](http://gocover.io/github.com/urfave/cli/altsrc) -cli is a simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way. +**Notice:** This is the library formerly known as +`github.com/codegangsta/cli` -- Github will automatically redirect requests +to this repository, but we recommend updating your references for clarity. + +cli is a simple, fast, and fun package for building command line apps in Go. The +goal is to enable developers to write fast and distributable command line +applications in an expressive way. + + + +- [Overview](#overview) +- [Installation](#installation) + * [Supported platforms](#supported-platforms) + * [Using the `v2` branch](#using-the-v2-branch) + * [Pinning to the `v1` releases](#pinning-to-the-v1-releases) +- [Getting Started](#getting-started) +- [Examples](#examples) + * [Arguments](#arguments) + * [Flags](#flags) + + [Placeholder Values](#placeholder-values) + + [Alternate Names](#alternate-names) + + [Ordering](#ordering) + + [Values from the Environment](#values-from-the-environment) + + [Values from alternate input sources (YAML, TOML, and others)](#values-from-alternate-input-sources-yaml-toml-and-others) + * [Subcommands](#subcommands) + * [Subcommands categories](#subcommands-categories) + * [Exit code](#exit-code) + * [Bash Completion](#bash-completion) + + [Enabling](#enabling) + + [Distribution](#distribution) + + [Customization](#customization) + * [Generated Help Text](#generated-help-text) + + [Customization](#customization-1) + * [Version Flag](#version-flag) + + [Customization](#customization-2) + + [Full API Example](#full-api-example) +- [Contribution Guidelines](#contribution-guidelines) + + ## Overview -Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app. +Command line apps are usually so tiny that there is absolutely no reason why +your code should *not* be self-documenting. Things like generating help text and +parsing command flags/options should not hinder productivity when writing a +command line app. -**This is where cli comes into play.** cli makes command line programming fun, organized, and expressive! +**This is where cli comes into play.** cli makes command line programming fun, +organized, and expressive! ## Installation -Make sure you have a working Go environment (go 1.1+ is *required*). [See the install instructions](http://golang.org/doc/install.html). +Make sure you have a working Go environment. Go version 1.2+ is supported. [See +the install instructions for Go](http://golang.org/doc/install.html). To install cli, simply run: ``` -$ go get github.com/codegangsta/cli +$ go get github.com/urfave/cli ``` -Make sure your `PATH` includes to the `$GOPATH/bin` directory so your commands can be easily used: +Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can +be easily used: ``` export PATH=$PATH:$GOPATH/bin ``` +### Supported platforms + +cli is tested against multiple versions of Go on Linux, and against the latest +released version of Go on OS X and Windows. For full details, see +[`./.travis.yml`](./.travis.yml) and [`./appveyor.yml`](./appveyor.yml). + +### Using the `v2` branch + +**Warning**: The `v2` branch is currently unreleased and considered unstable. + +There is currently a long-lived branch named `v2` that is intended to land as +the new `master` branch once development there has settled down. The current +`master` branch (mirrored as `v1`) is being manually merged into `v2` on +an irregular human-based schedule, but generally if one wants to "upgrade" to +`v2` *now* and accept the volatility (read: "awesomeness") that comes along with +that, please use whatever version pinning of your preference, such as via +`gopkg.in`: + +``` +$ go get gopkg.in/urfave/cli.v2 +``` + +``` go +... +import ( + "gopkg.in/urfave/cli.v2" // imports as package "cli" +) +... +``` + +### Pinning to the `v1` releases + +Similarly to the section above describing use of the `v2` branch, if one wants +to avoid any unexpected compatibility pains once `v2` becomes `master`, then +pinning to `v1` is an acceptable option, e.g.: + +``` +$ go get gopkg.in/urfave/cli.v1 +``` + +``` go +... +import ( + "gopkg.in/urfave/cli.v1" // imports as package "cli" +) +... +``` + +This will pull the latest tagged `v1` release (e.g. `v1.18.1` at the time of writing). + ## Getting Started -One of the philosophies behind cli is that an API should be playful and full of discovery. So a cli app can be as little as one line of code in `main()`. +One of the philosophies behind cli is that an API should be playful and full of +discovery. So a cli app can be as little as one line of code in `main()`. + ``` go package main import ( "os" - "github.com/codegangsta/cli" + + "github.com/urfave/cli" ) func main() { @@ -45,7 +148,8 @@ func main() { } ``` -This app will run and show help text, but is not very useful. Let's give an action to execute and some help documentation: +This app will run and show help text, but is not very useful. Let's give an +action to execute and some help documentation: ``` go -... -app.Action = func(c *cli.Context) error { - fmt.Println("Hello", c.Args()[0]) - return nil +package main + +import ( + "fmt" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Action = func(c *cli.Context) error { + fmt.Printf("Hello %q", c.Args().Get(0)) + return nil + } + + app.Run(os.Args) } -... ``` ### Flags Setting and querying flags is simple. + ``` go -... -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang", - Value: "english", - Usage: "language for the greeting", - }, -} -app.Action = func(c *cli.Context) error { - name := "someone" - if c.NArg() > 0 { - name = c.Args()[0] +package main + +import ( + "fmt" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + }, } - if c.String("lang") == "spanish" { - fmt.Println("Hola", name) - } else { - fmt.Println("Hello", name) + + app.Action = func(c *cli.Context) error { + name := "Nefertiti" + if c.NArg() > 0 { + name = c.Args().Get(0) + } + if c.String("lang") == "spanish" { + fmt.Println("Hola", name) + } else { + fmt.Println("Hello", name) + } + return nil } - return nil + + app.Run(os.Args) } -... ``` -You can also set a destination variable for a flag, to which the content will be scanned. +You can also set a destination variable for a flag, to which the content will be +scanned. + ``` go -... -var language string -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang", - Value: "english", - Usage: "language for the greeting", - Destination: &language, - }, -} -app.Action = func(c *cli.Context) error { - name := "someone" - if c.NArg() > 0 { - name = c.Args()[0] +package main + +import ( + "os" + "fmt" + + "github.com/urfave/cli" +) + +func main() { + var language string + + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + Destination: &language, + }, } - if language == "spanish" { - fmt.Println("Hola", name) - } else { - fmt.Println("Hello", name) + + app.Action = func(c *cli.Context) error { + name := "someone" + if c.NArg() > 0 { + name = c.Args()[0] + } + if language == "spanish" { + fmt.Println("Hola", name) + } else { + fmt.Println("Hello", name) + } + return nil } - return nil + + app.Run(os.Args) } -... ``` -See full list of flags at http://godoc.org/github.com/codegangsta/cli +See full list of flags at http://godoc.org/github.com/urfave/cli #### Placeholder Values -Sometimes it's useful to specify a flag's value within the usage string itself. Such placeholders are -indicated with back quotes. +Sometimes it's useful to specify a flag's value within the usage string itself. +Such placeholders are indicated with back quotes. For example this: + ```go -cli.StringFlag{ - Name: "config, c", - Usage: "Load configuration from `FILE`", +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "config, c", + Usage: "Load configuration from `FILE`", + }, + } + + app.Run(os.Args) } ``` @@ -231,145 +411,318 @@ Will result in help output like: --config FILE, -c FILE Load configuration from FILE ``` -Note that only the first placeholder is used. Subsequent back-quoted words will be left as-is. +Note that only the first placeholder is used. Subsequent back-quoted words will +be left as-is. #### Alternate Names -You can set alternate (or short) names for flags by providing a comma-delimited list for the `Name`. e.g. +You can set alternate (or short) names for flags by providing a comma-delimited +list for the `Name`. e.g. + ``` go -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - }, +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + }, + } + + app.Run(os.Args) } ``` -That flag can then be set with `--lang spanish` or `-l spanish`. Note that giving two different forms of the same flag in the same command invocation is an error. +That flag can then be set with `--lang spanish` or `-l spanish`. Note that +giving two different forms of the same flag in the same command invocation is an +error. + +#### Ordering + +Flags for the application and commands are shown in the order they are defined. +However, it's possible to sort them from outside this library by using `FlagsByName` +or `CommandsByName` with `sort`. + +For example this: + + +``` go +package main + +import ( + "os" + "sort" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "Language for the greeting", + }, + cli.StringFlag{ + Name: "config, c", + Usage: "Load configuration from `FILE`", + }, + } + + app.Commands = []cli.Command{ + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) error { + return nil + }, + }, + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(c *cli.Context) error { + return nil + }, + }, + } + + sort.Sort(cli.FlagsByName(app.Flags)) + sort.Sort(cli.CommandsByName(app.Commands)) + + app.Run(os.Args) +} +``` + +Will result in help output like: + +``` +--config FILE, -c FILE Load configuration from FILE +--lang value, -l value Language for the greeting (default: "english") +``` #### Values from the Environment You can also have the default value set from the environment via `EnvVar`. e.g. + ``` go -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - EnvVar: "APP_LANG", - }, +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + EnvVar: "APP_LANG", + }, + } + + app.Run(os.Args) } ``` -The `EnvVar` may also be given as a comma-delimited "cascade", where the first environment variable that resolves is used as the default. +The `EnvVar` may also be given as a comma-delimited "cascade", where the first +environment variable that resolves is used as the default. + ``` go -app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - EnvVar: "LEGACY_COMPAT_LANG,APP_LANG,LANG", - }, +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + EnvVar: "LEGACY_COMPAT_LANG,APP_LANG,LANG", + }, + } + + app.Run(os.Args) } ``` -#### Values from alternate input sources (YAML and others) +#### Values from alternate input sources (YAML, TOML, and others) -There is a separate package altsrc that adds support for getting flag values from other input sources like YAML. +There is a separate package altsrc that adds support for getting flag values +from other file input sources. -In order to get values for a flag from an alternate input source the following code would be added to wrap an existing cli.Flag like below: +Currently supported input source formats: +* YAML +* TOML + +In order to get values for a flag from an alternate input source the following +code would be added to wrap an existing cli.Flag like below: ``` go altsrc.NewIntFlag(cli.IntFlag{Name: "test"}) ``` -Initialization must also occur for these flags. Below is an example initializing getting data from a yaml file below. +Initialization must also occur for these flags. Below is an example initializing +getting data from a yaml file below. ``` go command.Before = altsrc.InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) ``` -The code above will use the "load" string as a flag name to get the file name of a yaml file from the cli.Context. -It will then use that file name to initialize the yaml input source for any flags that are defined on that command. -As a note the "load" flag used would also have to be defined on the command flags in order for this code snipped to work. +The code above will use the "load" string as a flag name to get the file name of +a yaml file from the cli.Context. It will then use that file name to initialize +the yaml input source for any flags that are defined on that command. As a note +the "load" flag used would also have to be defined on the command flags in order +for this code snipped to work. -Currently only YAML files are supported but developers can add support for other input sources by implementing the +Currently only the aboved specified formats are supported but developers can +add support for other input sources by implementing the altsrc.InputSourceContext for their given sources. Here is a more complete sample of a command using YAML support: + ``` go - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - // Action to run - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(cli.IntFlag{Name: "test"}), - cli.StringFlag{Name: "load"}}, +package notmain + +import ( + "fmt" + "os" + + "github.com/urfave/cli" + "github.com/urfave/cli/altsrc" +) + +func main() { + app := cli.NewApp() + + flags := []cli.Flag{ + altsrc.NewIntFlag(cli.IntFlag{Name: "test"}), + cli.StringFlag{Name: "load"}, } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) + + app.Action = func(c *cli.Context) error { + fmt.Println("yaml ist rad") + return nil + } + + app.Before = altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")) + app.Flags = flags + + app.Run(os.Args) +} ``` ### Subcommands Subcommands can be defined for a more git-like command line app. + ```go -... -app.Commands = []cli.Command{ - { - Name: "add", - Aliases: []string{"a"}, - Usage: "add a task to the list", - Action: func(c *cli.Context) error { - fmt.Println("added task: ", c.Args().First()) - return nil - }, - }, - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(c *cli.Context) error { - fmt.Println("completed task: ", c.Args().First()) - return nil - }, - }, - { - Name: "template", - Aliases: []string{"r"}, - Usage: "options for task templates", - Subcommands: []cli.Command{ - { - Name: "add", - Usage: "add a new template", - Action: func(c *cli.Context) error { - fmt.Println("new task template: ", c.Args().First()) - return nil - }, +package main + +import ( + "fmt" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Commands = []cli.Command{ + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(c *cli.Context) error { + fmt.Println("added task: ", c.Args().First()) + return nil }, - { - Name: "remove", - Usage: "remove an existing template", - Action: func(c *cli.Context) error { - fmt.Println("removed task template: ", c.Args().First()) - return nil + }, + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) error { + fmt.Println("completed task: ", c.Args().First()) + return nil + }, + }, + { + Name: "template", + Aliases: []string{"t"}, + Usage: "options for task templates", + Subcommands: []cli.Command{ + { + Name: "add", + Usage: "add a new template", + Action: func(c *cli.Context) error { + fmt.Println("new task template: ", c.Args().First()) + return nil + }, + }, + { + Name: "remove", + Usage: "remove an existing template", + Action: func(c *cli.Context) error { + fmt.Println("removed task template: ", c.Args().First()) + return nil + }, }, }, }, - }, + } + + app.Run(os.Args) } -... ``` ### Subcommands categories @@ -381,7 +734,17 @@ output. E.g. ```go -... +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.Commands = []cli.Command{ { Name: "noop", @@ -395,20 +758,20 @@ E.g. Category: "template", }, } -... + + app.Run(os.Args) +} ``` Will include: ``` -... COMMANDS: noop Template actions: add remove -... ``` ### Exit code @@ -424,7 +787,7 @@ package main import ( "os" - "github.com/codegangsta/cli" + "github.com/urfave/cli" ) func main() { @@ -453,42 +816,58 @@ flag on the `App` object. By default, this setting will only auto-complete to show an app's subcommands, but you can write your own completion methods for the App or its subcommands. -```go -... -var tasks = []string{"cook", "clean", "laundry", "eat", "sleep", "code"} -app := cli.NewApp() -app.EnableBashCompletion = true -app.Commands = []cli.Command{ - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(c *cli.Context) error { - fmt.Println("completed task: ", c.Args().First()) - return nil - }, - BashComplete: func(c *cli.Context) { - // This will complete if no args are passed - if c.NArg() > 0 { - return - } - for _, t := range tasks { - fmt.Println(t) - } + +``` go +package main + +import ( + "fmt" + "os" + + "github.com/urfave/cli" +) + +func main() { + tasks := []string{"cook", "clean", "laundry", "eat", "sleep", "code"} + + app := cli.NewApp() + app.EnableBashCompletion = true + app.Commands = []cli.Command{ + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) error { + fmt.Println("completed task: ", c.Args().First()) + return nil + }, + BashComplete: func(c *cli.Context) { + // This will complete if no args are passed + if c.NArg() > 0 { + return + } + for _, t := range tasks { + fmt.Println(t) + } + }, }, } + + app.Run(os.Args) } -... ``` -#### To Enable +#### Enabling Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while setting the `PROG` variable to the name of your program: `PROG=myprogram source /.../cli/autocomplete/bash_autocomplete` -#### To Distribute +#### Distribution Copy `autocomplete/bash_autocomplete` into `/etc/bash_completion.d/` and rename it to the name of the program you wish to add autocomplete support for (or @@ -504,7 +883,48 @@ Alternatively, you can just document that users should source the generic `autocomplete/bash_autocomplete` in their bash configuration with `$PROG` set to the name of their program (as above). -### Generated Help Text Customization +#### Customization + +The default bash completion flag (`--generate-bash-completion`) is defined as +`cli.BashCompletionFlag`, and may be redefined if desired, e.g.: + + +``` go +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.BashCompletionFlag = cli.BoolFlag{ + Name: "compgen", + Hidden: true, + } + + app := cli.NewApp() + app.EnableBashCompletion = true + app.Commands = []cli.Command{ + { + Name: "wat", + }, + } + app.Run(os.Args) +} +``` + +### Generated Help Text + +The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked +by the cli internals in order to print generated help text for the app, command, +or subcommand, and break execution. + +#### Customization All of the help text generation may be customized, and at multiple levels. The templates are exposed as variables `AppHelpTemplate`, `CommandHelpTemplate`, and @@ -523,7 +943,7 @@ import ( "io" "os" - "github.com/codegangsta/cli" + "github.com/urfave/cli" ) func main() { @@ -540,16 +960,13 @@ SUPPORT: support@awesometown.example.com cli.AppHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: - {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command -[command options]{{end}} {{if -.ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} {{if len .Authors}} -AUTHOR(S): +AUTHOR: {{range .Authors}}{{ . }}{{end}} {{end}}{{if .Commands}} COMMANDS: -{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t" -}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} +{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} GLOBAL OPTIONS: {{range .VisibleFlags}}{{.}} {{end}}{{end}}{{if .Copyright }} @@ -570,10 +987,395 @@ VERSION: } ``` +The default flag may be customized to something other than `-h/--help` by +setting `cli.HelpFlag`, e.g.: + + +``` go +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.HelpFlag = cli.BoolFlag{ + Name: "halp, haaaaalp", + Usage: "HALP", + EnvVar: "SHOW_HALP,HALPPLZ", + } + + cli.NewApp().Run(os.Args) +} +``` + +### Version Flag + +The default version flag (`-v/--version`) is defined as `cli.VersionFlag`, which +is checked by the cli internals in order to print the `App.Version` via +`cli.VersionPrinter` and break execution. + +#### Customization + +The default flag may be customized to something other than `-v/--version` by +setting `cli.VersionFlag`, e.g.: + + +``` go +package main + +import ( + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.VersionFlag = cli.BoolFlag{ + Name: "print-version, V", + Usage: "print only the version", + } + + app := cli.NewApp() + app.Name = "partay" + app.Version = "19.99.0" + app.Run(os.Args) +} +``` + +Alternatively, the version printer at `cli.VersionPrinter` may be overridden, e.g.: + + +``` go +package main + +import ( + "fmt" + "os" + + "github.com/urfave/cli" +) + +var ( + Revision = "fafafaf" +) + +func main() { + cli.VersionPrinter = func(c *cli.Context) { + fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision) + } + + app := cli.NewApp() + app.Name = "partay" + app.Version = "19.99.0" + app.Run(os.Args) +} +``` + +#### Full API Example + +**Notice**: This is a contrived (functioning) example meant strictly for API +demonstration purposes. Use of one's imagination is encouraged. + + +``` go +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "time" + + "github.com/urfave/cli" +) + +func init() { + cli.AppHelpTemplate += "\nCUSTOMIZED: you bet ur muffins\n" + cli.CommandHelpTemplate += "\nYMMV\n" + cli.SubcommandHelpTemplate += "\nor something\n" + + cli.HelpFlag = cli.BoolFlag{Name: "halp"} + cli.BashCompletionFlag = cli.BoolFlag{Name: "compgen", Hidden: true} + cli.VersionFlag = cli.BoolFlag{Name: "print-version, V"} + + cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { + fmt.Fprintf(w, "best of luck to you\n") + } + cli.VersionPrinter = func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "version=%s\n", c.App.Version) + } + cli.OsExiter = func(c int) { + fmt.Fprintf(cli.ErrWriter, "refusing to exit %d\n", c) + } + cli.ErrWriter = ioutil.Discard + cli.FlagStringer = func(fl cli.Flag) string { + return fmt.Sprintf("\t\t%s", fl.GetName()) + } +} + +type hexWriter struct{} + +func (w *hexWriter) Write(p []byte) (int, error) { + for _, b := range p { + fmt.Printf("%x", b) + } + fmt.Printf("\n") + + return len(p), nil +} + +type genericType struct{ + s string +} + +func (g *genericType) Set(value string) error { + g.s = value + return nil +} + +func (g *genericType) String() string { + return g.s +} + +func main() { + app := cli.NewApp() + app.Name = "kənˈtrīv" + app.Version = "19.99.0" + app.Compiled = time.Now() + app.Authors = []cli.Author{ + cli.Author{ + Name: "Example Human", + Email: "human@example.com", + }, + } + app.Copyright = "(c) 1999 Serious Enterprise" + app.HelpName = "contrive" + app.Usage = "demonstrate available API" + app.UsageText = "contrive - demonstrating the available API" + app.ArgsUsage = "[args and such]" + app.Commands = []cli.Command{ + cli.Command{ + Name: "doo", + Aliases: []string{"do"}, + Category: "motion", + Usage: "do the doo", + UsageText: "doo - does the dooing", + Description: "no really, there is a lot of dooing to be done", + ArgsUsage: "[arrgh]", + Flags: []cli.Flag{ + cli.BoolFlag{Name: "forever, forevvarr"}, + }, + Subcommands: cli.Commands{ + cli.Command{ + Name: "wop", + Action: wopAction, + }, + }, + SkipFlagParsing: false, + HideHelp: false, + Hidden: false, + HelpName: "doo!", + BashComplete: func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "--better\n") + }, + Before: func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "brace for impact\n") + return nil + }, + After: func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "did we lose anyone?\n") + return nil + }, + Action: func(c *cli.Context) error { + c.Command.FullName() + c.Command.HasName("wop") + c.Command.Names() + c.Command.VisibleFlags() + fmt.Fprintf(c.App.Writer, "dodododododoodododddooooododododooo\n") + if c.Bool("forever") { + c.Command.Run(c) + } + return nil + }, + OnUsageError: func(c *cli.Context, err error, isSubcommand bool) error { + fmt.Fprintf(c.App.Writer, "for shame\n") + return err + }, + }, + } + app.Flags = []cli.Flag{ + cli.BoolFlag{Name: "fancy"}, + cli.BoolTFlag{Name: "fancier"}, + cli.DurationFlag{Name: "howlong, H", Value: time.Second * 3}, + cli.Float64Flag{Name: "howmuch"}, + cli.GenericFlag{Name: "wat", Value: &genericType{}}, + cli.Int64Flag{Name: "longdistance"}, + cli.Int64SliceFlag{Name: "intervals"}, + cli.IntFlag{Name: "distance"}, + cli.IntSliceFlag{Name: "times"}, + cli.StringFlag{Name: "dance-move, d"}, + cli.StringSliceFlag{Name: "names, N"}, + cli.UintFlag{Name: "age"}, + cli.Uint64Flag{Name: "bigage"}, + } + app.EnableBashCompletion = true + app.HideHelp = false + app.HideVersion = false + app.BashComplete = func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "lipstick\nkiss\nme\nlipstick\nringo\n") + } + app.Before = func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "HEEEERE GOES\n") + return nil + } + app.After = func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "Phew!\n") + return nil + } + app.CommandNotFound = func(c *cli.Context, command string) { + fmt.Fprintf(c.App.Writer, "Thar be no %q here.\n", command) + } + app.OnUsageError = func(c *cli.Context, err error, isSubcommand bool) error { + if isSubcommand { + return err + } + + fmt.Fprintf(c.App.Writer, "WRONG: %#v\n", err) + return nil + } + app.Action = func(c *cli.Context) error { + cli.DefaultAppComplete(c) + cli.HandleExitCoder(errors.New("not an exit coder, though")) + cli.ShowAppHelp(c) + cli.ShowCommandCompletions(c, "nope") + cli.ShowCommandHelp(c, "also-nope") + cli.ShowCompletions(c) + cli.ShowSubcommandHelp(c) + cli.ShowVersion(c) + + categories := c.App.Categories() + categories.AddCommand("sounds", cli.Command{ + Name: "bloop", + }) + + for _, category := range c.App.Categories() { + fmt.Fprintf(c.App.Writer, "%s\n", category.Name) + fmt.Fprintf(c.App.Writer, "%#v\n", category.Commands) + fmt.Fprintf(c.App.Writer, "%#v\n", category.VisibleCommands()) + } + + fmt.Printf("%#v\n", c.App.Command("doo")) + if c.Bool("infinite") { + c.App.Run([]string{"app", "doo", "wop"}) + } + + if c.Bool("forevar") { + c.App.RunAsSubcommand(c) + } + c.App.Setup() + fmt.Printf("%#v\n", c.App.VisibleCategories()) + fmt.Printf("%#v\n", c.App.VisibleCommands()) + fmt.Printf("%#v\n", c.App.VisibleFlags()) + + fmt.Printf("%#v\n", c.Args().First()) + if len(c.Args()) > 0 { + fmt.Printf("%#v\n", c.Args()[1]) + } + fmt.Printf("%#v\n", c.Args().Present()) + fmt.Printf("%#v\n", c.Args().Tail()) + + set := flag.NewFlagSet("contrive", 0) + nc := cli.NewContext(c.App, set, c) + + fmt.Printf("%#v\n", nc.Args()) + fmt.Printf("%#v\n", nc.Bool("nope")) + fmt.Printf("%#v\n", nc.BoolT("nerp")) + fmt.Printf("%#v\n", nc.Duration("howlong")) + fmt.Printf("%#v\n", nc.Float64("hay")) + fmt.Printf("%#v\n", nc.Generic("bloop")) + fmt.Printf("%#v\n", nc.Int64("bonk")) + fmt.Printf("%#v\n", nc.Int64Slice("burnks")) + fmt.Printf("%#v\n", nc.Int("bips")) + fmt.Printf("%#v\n", nc.IntSlice("blups")) + fmt.Printf("%#v\n", nc.String("snurt")) + fmt.Printf("%#v\n", nc.StringSlice("snurkles")) + fmt.Printf("%#v\n", nc.Uint("flub")) + fmt.Printf("%#v\n", nc.Uint64("florb")) + fmt.Printf("%#v\n", nc.GlobalBool("global-nope")) + fmt.Printf("%#v\n", nc.GlobalBoolT("global-nerp")) + fmt.Printf("%#v\n", nc.GlobalDuration("global-howlong")) + fmt.Printf("%#v\n", nc.GlobalFloat64("global-hay")) + fmt.Printf("%#v\n", nc.GlobalGeneric("global-bloop")) + fmt.Printf("%#v\n", nc.GlobalInt("global-bips")) + fmt.Printf("%#v\n", nc.GlobalIntSlice("global-blups")) + fmt.Printf("%#v\n", nc.GlobalString("global-snurt")) + fmt.Printf("%#v\n", nc.GlobalStringSlice("global-snurkles")) + + fmt.Printf("%#v\n", nc.FlagNames()) + fmt.Printf("%#v\n", nc.GlobalFlagNames()) + fmt.Printf("%#v\n", nc.GlobalIsSet("wat")) + fmt.Printf("%#v\n", nc.GlobalSet("wat", "nope")) + fmt.Printf("%#v\n", nc.NArg()) + fmt.Printf("%#v\n", nc.NumFlags()) + fmt.Printf("%#v\n", nc.Parent()) + + nc.Set("wat", "also-nope") + + ec := cli.NewExitError("ohwell", 86) + fmt.Fprintf(c.App.Writer, "%d", ec.ExitCode()) + fmt.Printf("made it!\n") + return ec + } + + if os.Getenv("HEXY") != "" { + app.Writer = &hexWriter{} + app.ErrWriter = &hexWriter{} + } + + app.Metadata = map[string]interface{}{ + "layers": "many", + "explicable": false, + "whatever-values": 19.99, + } + + app.Run(os.Args) +} + +func wopAction(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, ":wave: over here, eh\n") + return nil +} +``` + ## Contribution Guidelines -Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch. +Feel free to put up a pull request to fix a bug or maybe add a feature. I will +give it a code review and make sure that it does not break backwards +compatibility. If I or any other collaborators agree that it is in line with +the vision of the project, we will work with you to get the code into +a mergeable state and merge it into the master branch. -If you have contributed something significant to the project, I will most likely add you as a collaborator. As a collaborator you are given the ability to merge others pull requests. It is very important that new code does not break existing code, so be careful about what code you do choose to merge. If you have any questions feel free to link @codegangsta to the issue in question and we can review it together. +If you have contributed something significant to the project, we will most +likely add you as a collaborator. As a collaborator you are given the ability +to merge others pull requests. It is very important that new code does not +break existing code, so be careful about what code you do choose to merge. -If you feel like you have contributed to the project but have not yet been added as a collaborator, I probably forgot to add you. Hit @codegangsta up over email and we will get it figured out. +If you feel like you have contributed to the project but have not yet been +added as a collaborator, we probably forgot to add you, please open an issue. diff --git a/vendor/github.com/urfave/cli/app.go b/vendor/github.com/urfave/cli/app.go index 7c9b9580..51fc45d8 100644 --- a/vendor/github.com/urfave/cli/app.go +++ b/vendor/github.com/urfave/cli/app.go @@ -6,23 +6,19 @@ import ( "io/ioutil" "os" "path/filepath" - "reflect" "sort" "time" ) var ( - changeLogURL = "https://github.com/codegangsta/cli/blob/master/CHANGELOG.md" + changeLogURL = "https://github.com/urfave/cli/blob/master/CHANGELOG.md" appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL) runAndExitOnErrorDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-runandexitonerror", changeLogURL) contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you." - errNonFuncAction = NewExitError("ERROR invalid Action type. "+ - fmt.Sprintf("Must be a func of type `cli.ActionFunc`. %s", contactSysadmin)+ - fmt.Sprintf("See %s", appActionDeprecationURL), 2) - errInvalidActionSignature = NewExitError("ERROR invalid Action signature. "+ - fmt.Sprintf("Must be `cli.ActionFunc`. %s", contactSysadmin)+ + errInvalidActionType = NewExitError("ERROR invalid Action type. "+ + fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+ fmt.Sprintf("See %s", appActionDeprecationURL), 2) ) @@ -41,6 +37,8 @@ type App struct { ArgsUsage string // Version of the program Version string + // Description of the program + Description string // List of commands to execute Commands []Command // List of flags to parse @@ -61,10 +59,11 @@ type App struct { // An action to execute after any subcommands are run, but after the subcommand has finished // It is run even if Action() panics After AfterFunc + // The action to execute when no subcommands are specified + // Expects a `cli.ActionFunc` but will accept the *deprecated* signature of `func(*cli.Context) {}` + // *Note*: support for the deprecated `Action` signature will be removed in a future version Action interface{} - // TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind - // of deprecation period has passed, maybe? // Execute this function if the proper command cannot be found CommandNotFound CommandNotFoundFunc @@ -86,6 +85,12 @@ type App struct { ErrWriter io.Writer // Other custom info Metadata map[string]interface{} + // Carries a function which returns app specific info. + ExtraInfo func() map[string]string + // CustomAppHelpTemplate the text template for app help topic. + // cli.go uses text/template to render templates. You can + // render custom help text by setting this variable. + CustomAppHelpTemplate string didSetup bool } @@ -139,13 +144,6 @@ func (a *App) Setup() { } a.Commands = newCmds - a.categories = CommandCategories{} - for _, command := range a.Commands { - a.categories = a.categories.AddCommand(command.Category, command) - } - sort.Sort(a.categories) - - // append help to commands if a.Command(helpCommand.Name) == nil && !a.HideHelp { a.Commands = append(a.Commands, helpCommand) if (HelpFlag != BoolFlag{}) { @@ -153,14 +151,23 @@ func (a *App) Setup() { } } - //append version/help flags - if a.EnableBashCompletion { - a.appendFlag(BashCompletionFlag) - } - if !a.HideVersion { a.appendFlag(VersionFlag) } + + a.categories = CommandCategories{} + for _, command := range a.Commands { + a.categories = a.categories.AddCommand(command.Category, command) + } + sort.Sort(a.categories) + + if a.Metadata == nil { + a.Metadata = make(map[string]interface{}) + } + + if a.Writer == nil { + a.Writer = os.Stdout + } } // Run is the entry point to the cli app. Parses the arguments slice and routes @@ -168,8 +175,20 @@ func (a *App) Setup() { func (a *App) Run(arguments []string) (err error) { a.Setup() + // handle the completion flag separately from the flagset since + // completion could be attempted after a flag, but before its value was put + // on the command line. this causes the flagset to interpret the completion + // flag name as the value of the flag before it which is undesirable + // note that we can only do this because the shell autocomplete function + // always appends the completion flag at the end of the command + shellComplete, arguments := checkShellCompleteFlag(a, arguments) + // parse flags - set := flagSet(a.Name, a.Flags) + set, err := flagSet(a.Name, a.Flags) + if err != nil { + return err + } + set.SetOutput(ioutil.Discard) err = set.Parse(arguments[1:]) nerr := normalizeFlags(a.Flags, set) @@ -179,6 +198,7 @@ func (a *App) Run(arguments []string) (err error) { ShowAppHelp(context) return nerr } + context.shellComplete = shellComplete if checkCompletions(context) { return nil @@ -190,7 +210,7 @@ func (a *App) Run(arguments []string) (err error) { HandleExitCoder(err) return err } - fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") + fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) ShowAppHelp(context) return err } @@ -220,7 +240,6 @@ func (a *App) Run(arguments []string) (err error) { if a.Before != nil { beforeErr := a.Before(context) if beforeErr != nil { - fmt.Fprintf(a.Writer, "%v\n\n", beforeErr) ShowAppHelp(context) HandleExitCoder(beforeErr) err = beforeErr @@ -237,6 +256,10 @@ func (a *App) Run(arguments []string) (err error) { } } + if a.Action == nil { + a.Action = helpCommand.Action + } + // Run default Action err = HandleAction(a.Action, context) @@ -244,11 +267,12 @@ func (a *App) Run(arguments []string) (err error) { return err } -// DEPRECATED: Another entry point to the cli app, takes care of passing arguments and error handling +// RunAndExitOnError calls .Run() and exits non-zero if an error was returned +// +// Deprecated: instead you should return an error that fulfills cli.ExitCoder +// to cli.App.Run. This will cause the application to exit with the given eror +// code in the cli.ExitCoder func (a *App) RunAndExitOnError() { - fmt.Fprintf(a.errWriter(), - "DEPRECATED cli.App.RunAndExitOnError. %s See %s\n", - contactSysadmin, runAndExitOnErrorDeprecationURL) if err := a.Run(os.Args); err != nil { fmt.Fprintln(a.errWriter(), err) OsExiter(1) @@ -277,13 +301,12 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } a.Commands = newCmds - // append flags - if a.EnableBashCompletion { - a.appendFlag(BashCompletionFlag) + // parse flags + set, err := flagSet(a.Name, a.Flags) + if err != nil { + return err } - // parse flags - set := flagSet(a.Name, a.Flags) set.SetOutput(ioutil.Discard) err = set.Parse(ctx.Args().Tail()) nerr := normalizeFlags(a.Flags, set) @@ -310,7 +333,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { HandleExitCoder(err) return err } - fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") + fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) ShowSubcommandHelp(context) return err } @@ -451,48 +474,24 @@ type Author struct { func (a Author) String() string { e := "" if a.Email != "" { - e = "<" + a.Email + "> " + e = " <" + a.Email + ">" } - return fmt.Sprintf("%v %v", a.Name, e) + return fmt.Sprintf("%v%v", a.Name, e) } -// HandleAction uses ✧✧✧reflection✧✧✧ to figure out if the given Action is an -// ActionFunc, a func with the legacy signature for Action, or some other -// invalid thing. If it's an ActionFunc or a func with the legacy signature for -// Action, the func is run! +// HandleAction attempts to figure out which Action signature was used. If +// it's an ActionFunc or a func with the legacy signature for Action, the func +// is run! func HandleAction(action interface{}, context *Context) (err error) { - defer func() { - if r := recover(); r != nil { - switch r.(type) { - case error: - err = r.(error) - default: - err = NewExitError(fmt.Sprintf("ERROR unknown Action error: %v. See %s", r, appActionDeprecationURL), 2) - } - } - }() - - if reflect.TypeOf(action).Kind() != reflect.Func { - return errNonFuncAction - } - - vals := reflect.ValueOf(action).Call([]reflect.Value{reflect.ValueOf(context)}) - - if len(vals) == 0 { - fmt.Fprintf(ErrWriter, - "DEPRECATED Action signature. Must be `cli.ActionFunc`. %s See %s\n", - contactSysadmin, appActionDeprecationURL) + if a, ok := action.(ActionFunc); ok { + return a(context) + } else if a, ok := action.(func(*Context) error); ok { + return a(context) + } else if a, ok := action.(func(*Context)); ok { // deprecated function signature + a(context) return nil + } else { + return errInvalidActionType } - - if len(vals) > 1 { - return errInvalidActionSignature - } - - if retErr, ok := vals[0].Interface().(error); vals[0].IsValid() && ok { - return retErr - } - - return err } diff --git a/vendor/github.com/urfave/cli/cli.go b/vendor/github.com/urfave/cli/cli.go index f0440c56..90c07eb8 100644 --- a/vendor/github.com/urfave/cli/cli.go +++ b/vendor/github.com/urfave/cli/cli.go @@ -12,8 +12,11 @@ // app.Usage = "say a greeting" // app.Action = func(c *cli.Context) error { // println("Greetings") +// return nil // } // // app.Run(os.Args) // } package cli + +//go:generate python ./generate-flag-types cli -i flag-types.json -o flag_generated.go diff --git a/vendor/github.com/urfave/cli/command.go b/vendor/github.com/urfave/cli/command.go index 8950ccae..23de2944 100644 --- a/vendor/github.com/urfave/cli/command.go +++ b/vendor/github.com/urfave/cli/command.go @@ -46,6 +46,11 @@ type Command struct { Flags []Flag // Treat all flags as normal arguments if true SkipFlagParsing bool + // Skip argument reordering which attempts to move flags before arguments, + // but only works if all flags appear after all arguments. This behavior was + // removed n version 2 since it only works under specific conditions so we + // backport here by exposing it as an option for compatibility. + SkipArgReorder bool // Boolean to hide built-in help command HideHelp bool // Boolean to hide this command from help or completion @@ -54,6 +59,25 @@ type Command struct { // Full name of command for help, defaults to full command name, including parent commands. HelpName string commandNamePath []string + + // CustomHelpTemplate the text template for the command help topic. + // cli.go uses text/template to render templates. You can + // render custom help text by setting this variable. + CustomHelpTemplate string +} + +type CommandsByName []Command + +func (c CommandsByName) Len() int { + return len(c) +} + +func (c CommandsByName) Less(i, j int) bool { + return c[i].Name < c[j].Name +} + +func (c CommandsByName) Swap(i, j int) { + c[i], c[j] = c[j], c[i] } // FullName returns the full name of the command. @@ -82,14 +106,15 @@ func (c Command) Run(ctx *Context) (err error) { ) } - if ctx.App.EnableBashCompletion { - c.Flags = append(c.Flags, BashCompletionFlag) + set, err := flagSet(c.Name, c.Flags) + if err != nil { + return err } - - set := flagSet(c.Name, c.Flags) set.SetOutput(ioutil.Discard) - if !c.SkipFlagParsing { + if c.SkipFlagParsing { + err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...)) + } else if !c.SkipArgReorder { firstFlagIndex := -1 terminatorIndex := -1 for index, arg := range ctx.Args() { @@ -122,21 +147,7 @@ func (c Command) Run(ctx *Context) (err error) { err = set.Parse(ctx.Args().Tail()) } } else { - if c.SkipFlagParsing { - err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...)) - } - } - - if err != nil { - if c.OnUsageError != nil { - err := c.OnUsageError(ctx, err, false) - HandleExitCoder(err) - return err - } - fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.") - fmt.Fprintln(ctx.App.Writer) - ShowCommandHelp(ctx, c.Name) - return err + err = set.Parse(ctx.Args().Tail()) } nerr := normalizeFlags(c.Flags, set) @@ -148,11 +159,23 @@ func (c Command) Run(ctx *Context) (err error) { } context := NewContext(ctx.App, set, ctx) - + context.Command = c if checkCommandCompletions(context, c.Name) { return nil } + if err != nil { + if c.OnUsageError != nil { + err := c.OnUsageError(context, err, false) + HandleExitCoder(err) + return err + } + fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) + fmt.Fprintln(context.App.Writer) + ShowCommandHelp(context, c.Name) + return err + } + if checkCommandHelp(context, c.Name) { return nil } @@ -174,15 +197,16 @@ func (c Command) Run(ctx *Context) (err error) { if c.Before != nil { err = c.Before(context) if err != nil { - fmt.Fprintln(ctx.App.Writer, err) - fmt.Fprintln(ctx.App.Writer) - ShowCommandHelp(ctx, c.Name) + ShowCommandHelp(context, c.Name) HandleExitCoder(err) return err } } - context.Command = c + if c.Action == nil { + c.Action = helpSubcommand.Action + } + err = HandleAction(c.Action, context) if err != nil { @@ -223,14 +247,13 @@ func (c Command) startApp(ctx *Context) error { app.HelpName = app.Name } - if c.Description != "" { - app.Usage = c.Description - } else { - app.Usage = c.Usage - } + app.Usage = c.Usage + app.Description = c.Description + app.ArgsUsage = c.ArgsUsage // set CommandNotFound app.CommandNotFound = ctx.App.CommandNotFound + app.CustomAppHelpTemplate = c.CustomHelpTemplate // set the flags and commands app.Commands = c.Subcommands @@ -243,6 +266,7 @@ func (c Command) startApp(ctx *Context) error { app.Author = ctx.App.Author app.Email = ctx.App.Email app.Writer = ctx.App.Writer + app.ErrWriter = ctx.App.ErrWriter app.categories = CommandCategories{} for _, command := range c.Subcommands { @@ -265,6 +289,7 @@ func (c Command) startApp(ctx *Context) error { } else { app.Action = helpSubcommand.Action } + app.OnUsageError = c.OnUsageError for index, cc := range app.Commands { app.Commands[index].commandNamePath = []string{c.Name, cc.Name} diff --git a/vendor/github.com/urfave/cli/context.go b/vendor/github.com/urfave/cli/context.go index c3424636..db94191e 100644 --- a/vendor/github.com/urfave/cli/context.go +++ b/vendor/github.com/urfave/cli/context.go @@ -3,9 +3,9 @@ package cli import ( "errors" "flag" - "strconv" + "reflect" "strings" - "time" + "syscall" ) // Context is a type that is passed through to @@ -13,147 +13,23 @@ import ( // can be used to retrieve context-specific Args and // parsed command-line options. type Context struct { - App *App - Command Command - flagSet *flag.FlagSet - setFlags map[string]bool - globalSetFlags map[string]bool - parentContext *Context + App *App + Command Command + shellComplete bool + flagSet *flag.FlagSet + setFlags map[string]bool + parentContext *Context } // NewContext creates a new context. For use in when invoking an App or Command action. func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { - return &Context{App: app, flagSet: set, parentContext: parentCtx} -} + c := &Context{App: app, flagSet: set, parentContext: parentCtx} -// Int looks up the value of a local int flag, returns 0 if no int flag exists -func (c *Context) Int(name string) int { - return lookupInt(name, c.flagSet) -} - -// Duration looks up the value of a local time.Duration flag, returns 0 if no -// time.Duration flag exists -func (c *Context) Duration(name string) time.Duration { - return lookupDuration(name, c.flagSet) -} - -// Float64 looks up the value of a local float64 flag, returns 0 if no float64 -// flag exists -func (c *Context) Float64(name string) float64 { - return lookupFloat64(name, c.flagSet) -} - -// Bool looks up the value of a local bool flag, returns false if no bool flag exists -func (c *Context) Bool(name string) bool { - return lookupBool(name, c.flagSet) -} - -// BoolT looks up the value of a local boolT flag, returns false if no bool flag exists -func (c *Context) BoolT(name string) bool { - return lookupBoolT(name, c.flagSet) -} - -// String looks up the value of a local string flag, returns "" if no string flag exists -func (c *Context) String(name string) string { - return lookupString(name, c.flagSet) -} - -// StringSlice looks up the value of a local string slice flag, returns nil if no -// string slice flag exists -func (c *Context) StringSlice(name string) []string { - return lookupStringSlice(name, c.flagSet) -} - -// IntSlice looks up the value of a local int slice flag, returns nil if no int -// slice flag exists -func (c *Context) IntSlice(name string) []int { - return lookupIntSlice(name, c.flagSet) -} - -// Generic looks up the value of a local generic flag, returns nil if no generic -// flag exists -func (c *Context) Generic(name string) interface{} { - return lookupGeneric(name, c.flagSet) -} - -// GlobalInt looks up the value of a global int flag, returns 0 if no int flag exists -func (c *Context) GlobalInt(name string) int { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupInt(name, fs) + if parentCtx != nil { + c.shellComplete = parentCtx.shellComplete } - return 0 -} -// GlobalFloat64 looks up the value of a global float64 flag, returns float64(0) -// if no float64 flag exists -func (c *Context) GlobalFloat64(name string) float64 { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupFloat64(name, fs) - } - return float64(0) -} - -// GlobalDuration looks up the value of a global time.Duration flag, returns 0 -// if no time.Duration flag exists -func (c *Context) GlobalDuration(name string) time.Duration { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupDuration(name, fs) - } - return 0 -} - -// GlobalBool looks up the value of a global bool flag, returns false if no bool -// flag exists -func (c *Context) GlobalBool(name string) bool { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupBool(name, fs) - } - return false -} - -// GlobalBoolT looks up the value of a global bool flag, returns true if no bool -// flag exists -func (c *Context) GlobalBoolT(name string) bool { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupBoolT(name, fs) - } - return false -} - -// GlobalString looks up the value of a global string flag, returns "" if no -// string flag exists -func (c *Context) GlobalString(name string) string { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupString(name, fs) - } - return "" -} - -// GlobalStringSlice looks up the value of a global string slice flag, returns -// nil if no string slice flag exists -func (c *Context) GlobalStringSlice(name string) []string { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupStringSlice(name, fs) - } - return nil -} - -// GlobalIntSlice looks up the value of a global int slice flag, returns nil if -// no int slice flag exists -func (c *Context) GlobalIntSlice(name string) []int { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupIntSlice(name, fs) - } - return nil -} - -// GlobalGeneric looks up the value of a global generic flag, returns nil if no -// generic flag exists -func (c *Context) GlobalGeneric(name string) interface{} { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupGeneric(name, fs) - } - return nil + return c } // NumFlags returns the number of flags set @@ -163,11 +39,13 @@ func (c *Context) NumFlags() int { // Set sets a context flag to a value. func (c *Context) Set(name, value string) error { + c.setFlags = nil return c.flagSet.Set(name, value) } // GlobalSet sets a context flag to a value on the global flagset func (c *Context) GlobalSet(name, value string) error { + globalContext(c).setFlags = nil return globalContext(c).flagSet.Set(name, value) } @@ -175,28 +53,78 @@ func (c *Context) GlobalSet(name, value string) error { func (c *Context) IsSet(name string) bool { if c.setFlags == nil { c.setFlags = make(map[string]bool) + c.flagSet.Visit(func(f *flag.Flag) { c.setFlags[f.Name] = true }) + + c.flagSet.VisitAll(func(f *flag.Flag) { + if _, ok := c.setFlags[f.Name]; ok { + return + } + c.setFlags[f.Name] = false + }) + + // XXX hack to support IsSet for flags with EnvVar + // + // There isn't an easy way to do this with the current implementation since + // whether a flag was set via an environment variable is very difficult to + // determine here. Instead, we intend to introduce a backwards incompatible + // change in version 2 to add `IsSet` to the Flag interface to push the + // responsibility closer to where the information required to determine + // whether a flag is set by non-standard means such as environment + // variables is avaliable. + // + // See https://github.com/urfave/cli/issues/294 for additional discussion + flags := c.Command.Flags + if c.Command.Name == "" { // cannot == Command{} since it contains slice types + if c.App != nil { + flags = c.App.Flags + } + } + for _, f := range flags { + eachName(f.GetName(), func(name string) { + if isSet, ok := c.setFlags[name]; isSet || !ok { + return + } + + val := reflect.ValueOf(f) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + envVarValue := val.FieldByName("EnvVar") + if !envVarValue.IsValid() { + return + } + + eachName(envVarValue.String(), func(envVar string) { + envVar = strings.TrimSpace(envVar) + if _, ok := syscall.Getenv(envVar); ok { + c.setFlags[name] = true + return + } + }) + }) + } } - return c.setFlags[name] == true + + return c.setFlags[name] } // GlobalIsSet determines if the global flag was actually set func (c *Context) GlobalIsSet(name string) bool { - if c.globalSetFlags == nil { - c.globalSetFlags = make(map[string]bool) - ctx := c - if ctx.parentContext != nil { - ctx = ctx.parentContext - } - for ; ctx != nil && c.globalSetFlags[name] == false; ctx = ctx.parentContext { - ctx.flagSet.Visit(func(f *flag.Flag) { - c.globalSetFlags[f.Name] = true - }) + ctx := c + if ctx.parentContext != nil { + ctx = ctx.parentContext + } + + for ; ctx != nil; ctx = ctx.parentContext { + if ctx.IsSet(name) { + return true } } - return c.globalSetFlags[name] + return false } // FlagNames returns a slice of flag names used in this context. @@ -228,6 +156,11 @@ func (c *Context) Parent() *Context { return c.parentContext } +// value returns the value of the flag coressponding to `name` +func (c *Context) value(name string) interface{} { + return c.flagSet.Lookup(name).Value.(flag.Getter).Get() +} + // Args contains apps console arguments type Args []string @@ -303,107 +236,6 @@ func lookupGlobalFlagSet(name string, ctx *Context) *flag.FlagSet { return nil } -func lookupInt(name string, set *flag.FlagSet) int { - f := set.Lookup(name) - if f != nil { - val, err := strconv.Atoi(f.Value.String()) - if err != nil { - return 0 - } - return val - } - - return 0 -} - -func lookupDuration(name string, set *flag.FlagSet) time.Duration { - f := set.Lookup(name) - if f != nil { - val, err := time.ParseDuration(f.Value.String()) - if err == nil { - return val - } - } - - return 0 -} - -func lookupFloat64(name string, set *flag.FlagSet) float64 { - f := set.Lookup(name) - if f != nil { - val, err := strconv.ParseFloat(f.Value.String(), 64) - if err != nil { - return 0 - } - return val - } - - return 0 -} - -func lookupString(name string, set *flag.FlagSet) string { - f := set.Lookup(name) - if f != nil { - return f.Value.String() - } - - return "" -} - -func lookupStringSlice(name string, set *flag.FlagSet) []string { - f := set.Lookup(name) - if f != nil { - return (f.Value.(*StringSlice)).Value() - - } - - return nil -} - -func lookupIntSlice(name string, set *flag.FlagSet) []int { - f := set.Lookup(name) - if f != nil { - return (f.Value.(*IntSlice)).Value() - - } - - return nil -} - -func lookupGeneric(name string, set *flag.FlagSet) interface{} { - f := set.Lookup(name) - if f != nil { - return f.Value - } - return nil -} - -func lookupBool(name string, set *flag.FlagSet) bool { - f := set.Lookup(name) - if f != nil { - val, err := strconv.ParseBool(f.Value.String()) - if err != nil { - return false - } - return val - } - - return false -} - -func lookupBoolT(name string, set *flag.FlagSet) bool { - f := set.Lookup(name) - if f != nil { - val, err := strconv.ParseBool(f.Value.String()) - if err != nil { - return true - } - return val - } - - return false -} - func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) { switch ff.Value.(type) { case *StringSlice: diff --git a/vendor/github.com/urfave/cli/errors.go b/vendor/github.com/urfave/cli/errors.go index ea551be1..562b2953 100644 --- a/vendor/github.com/urfave/cli/errors.go +++ b/vendor/github.com/urfave/cli/errors.go @@ -24,7 +24,7 @@ func NewMultiError(err ...error) MultiError { return MultiError{Errors: err} } -// Error implents the error interface. +// Error implements the error interface. func (m MultiError) Error() string { errs := make([]string, len(m.Errors)) for i, err := range m.Errors { @@ -34,6 +34,10 @@ func (m MultiError) Error() string { return strings.Join(errs, "\n") } +type ErrorFormatter interface { + Format(s fmt.State, verb rune) +} + // ExitCoder is the interface checked by `App` and `Command` for a custom exit // code type ExitCoder interface { @@ -44,11 +48,11 @@ type ExitCoder interface { // ExitError fulfills both the builtin `error` interface and `ExitCoder` type ExitError struct { exitCode int - message string + message interface{} } // NewExitError makes a new *ExitError -func NewExitError(message string, exitCode int) *ExitError { +func NewExitError(message interface{}, exitCode int) *ExitError { return &ExitError{ exitCode: exitCode, message: message, @@ -58,7 +62,7 @@ func NewExitError(message string, exitCode int) *ExitError { // Error returns the string message, fulfilling the interface required by // `error` func (ee *ExitError) Error() string { - return ee.message + return fmt.Sprintf("%v", ee.message) } // ExitCode returns the exit code, fulfilling the interface required by @@ -70,7 +74,7 @@ func (ee *ExitError) ExitCode() int { // HandleExitCoder checks if the error fulfills the ExitCoder interface, and if // so prints the error to stderr (if it is non-empty) and calls OsExiter with the // given exit code. If the given error is a MultiError, then this func is -// called on all members of the Errors slice. +// called on all members of the Errors slice and calls OsExiter with the last exit code. func HandleExitCoder(err error) { if err == nil { return @@ -78,15 +82,34 @@ func HandleExitCoder(err error) { if exitErr, ok := err.(ExitCoder); ok { if err.Error() != "" { - fmt.Fprintln(ErrWriter, err) + if _, ok := exitErr.(ErrorFormatter); ok { + fmt.Fprintf(ErrWriter, "%+v\n", err) + } else { + fmt.Fprintln(ErrWriter, err) + } } OsExiter(exitErr.ExitCode()) return } if multiErr, ok := err.(MultiError); ok { - for _, merr := range multiErr.Errors { - HandleExitCoder(merr) - } + code := handleMultiError(multiErr) + OsExiter(code) + return } } + +func handleMultiError(multiErr MultiError) int { + code := 1 + for _, merr := range multiErr.Errors { + if multiErr2, ok := merr.(MultiError); ok { + code = handleMultiError(multiErr2) + } else { + fmt.Fprintln(ErrWriter, merr) + if exitErr, ok := merr.(ExitCoder); ok { + code = exitErr.ExitCode() + } + } + } + return code +} diff --git a/vendor/github.com/urfave/cli/flag.go b/vendor/github.com/urfave/cli/flag.go index 1e8112e7..877ff352 100644 --- a/vendor/github.com/urfave/cli/flag.go +++ b/vendor/github.com/urfave/cli/flag.go @@ -3,24 +3,24 @@ package cli import ( "flag" "fmt" - "os" "reflect" "runtime" "strconv" "strings" + "syscall" "time" ) const defaultPlaceholder = "value" // BashCompletionFlag enables bash-completion for all commands and subcommands -var BashCompletionFlag = BoolFlag{ +var BashCompletionFlag Flag = BoolFlag{ Name: "generate-bash-completion", Hidden: true, } // VersionFlag prints the version for the application -var VersionFlag = BoolFlag{ +var VersionFlag Flag = BoolFlag{ Name: "version, v", Usage: "print the version", } @@ -28,7 +28,7 @@ var VersionFlag = BoolFlag{ // HelpFlag prints the help for all commands and subcommands // Set to the zero value (BoolFlag{}) to disable flag -- keeps subcommand // unless HideHelp is set to true) -var HelpFlag = BoolFlag{ +var HelpFlag Flag = BoolFlag{ Name: "help, h", Usage: "show help", } @@ -37,6 +37,21 @@ var HelpFlag = BoolFlag{ // to display a flag. var FlagStringer FlagStringFunc = stringifyFlag +// FlagsByName is a slice of Flag. +type FlagsByName []Flag + +func (f FlagsByName) Len() int { + return len(f) +} + +func (f FlagsByName) Less(i, j int) bool { + return f[i].GetName() < f[j].GetName() +} + +func (f FlagsByName) Swap(i, j int) { + f[i], f[j] = f[j], f[i] +} + // Flag is a common interface related to parsing flags in cli. // For more advanced flag parsing techniques, it is recommended that // this interface be implemented. @@ -47,13 +62,29 @@ type Flag interface { GetName() string } -func flagSet(name string, flags []Flag) *flag.FlagSet { +// errorableFlag is an interface that allows us to return errors during apply +// it allows flags defined in this library to return errors in a fashion backwards compatible +// TODO remove in v2 and modify the existing Flag interface to return errors +type errorableFlag interface { + Flag + + ApplyWithError(*flag.FlagSet) error +} + +func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { set := flag.NewFlagSet(name, flag.ContinueOnError) for _, f := range flags { - f.Apply(set) + //TODO remove in v2 when errorableFlag is removed + if ef, ok := f.(errorableFlag); ok { + if err := ef.ApplyWithError(set); err != nil { + return nil, err + } + } else { + f.Apply(set) + } } - return set + return set, nil } func eachName(longName string, fn func(string)) { @@ -70,31 +101,24 @@ type Generic interface { String() string } -// GenericFlag is the flag type for types implementing Generic -type GenericFlag struct { - Name string - Value Generic - Usage string - EnvVar string - Hidden bool -} - -// String returns the string representation of the generic flag to display the -// help text to the user (uses the String() method of the generic flag to show -// the value) -func (f GenericFlag) String() string { - return FlagStringer(f) -} - // Apply takes the flagset and calls Set on the generic flag with the value // provided by the user for parsing by the flag +// Ignores parsing errors func (f GenericFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError takes the flagset and calls Set on the generic flag with the value +// provided by the user for parsing by the flag +func (f GenericFlag) ApplyWithError(set *flag.FlagSet) error { val := f.Value if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - val.Set(envVal) + if envVal, ok := syscall.Getenv(envVar); ok { + if err := val.Set(envVal); err != nil { + return fmt.Errorf("could not parse %s as value for flag %s: %s", envVal, f.Name, err) + } break } } @@ -103,14 +127,11 @@ func (f GenericFlag) Apply(set *flag.FlagSet) { eachName(f.Name, func(name string) { set.Var(f.Value, name, f.Usage) }) + + return nil } -// GetName returns the name of a flag. -func (f GenericFlag) GetName() string { - return f.Name -} - -// StringSlice is an opaque type for []string to satisfy flag.Value +// StringSlice is an opaque type for []string to satisfy flag.Value and flag.Getter type StringSlice []string // Set appends the string value to the list of values @@ -129,31 +150,29 @@ func (f *StringSlice) Value() []string { return *f } -// StringSliceFlag is a string flag that can be specified multiple times on the -// command-line -type StringSliceFlag struct { - Name string - Value *StringSlice - Usage string - EnvVar string - Hidden bool -} - -// String returns the usage -func (f StringSliceFlag) String() string { - return FlagStringer(f) +// Get returns the slice of strings set by this flag +func (f *StringSlice) Get() interface{} { + return *f } // Apply populates the flag given the flag set and environment +// Ignores errors func (f StringSliceFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f StringSliceFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { newVal := &StringSlice{} for _, s := range strings.Split(envVal, ",") { s = strings.TrimSpace(s) - newVal.Set(s) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %s as string value for flag %s: %s", envVal, f.Name, err) + } } f.Value = newVal break @@ -167,14 +186,11 @@ func (f StringSliceFlag) Apply(set *flag.FlagSet) { } set.Var(f.Value, name, f.Usage) }) + + return nil } -// GetName returns the name of a flag. -func (f StringSliceFlag) GetName() string { - return f.Name -} - -// IntSlice is an opaque type for []int to satisfy flag.Value +// IntSlice is an opaque type for []int to satisfy flag.Value and flag.Getter type IntSlice []int // Set parses the value into an integer and appends it to the list of values @@ -189,7 +205,7 @@ func (f *IntSlice) Set(value string) error { // String returns a readable representation of this value (for usage defaults) func (f *IntSlice) String() string { - return fmt.Sprintf("%d", *f) + return fmt.Sprintf("%#v", *f) } // Value returns the slice of ints set by this flag @@ -197,33 +213,28 @@ func (f *IntSlice) Value() []int { return *f } -// IntSliceFlag is an int flag that can be specified multiple times on the -// command-line -type IntSliceFlag struct { - Name string - Value *IntSlice - Usage string - EnvVar string - Hidden bool -} - -// String returns the usage -func (f IntSliceFlag) String() string { - return FlagStringer(f) +// Get returns the slice of ints set by this flag +func (f *IntSlice) Get() interface{} { + return *f } // Apply populates the flag given the flag set and environment +// Ignores errors func (f IntSliceFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f IntSliceFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { newVal := &IntSlice{} for _, s := range strings.Split(envVal, ",") { s = strings.TrimSpace(s) - err := newVal.Set(s) - if err != nil { - fmt.Fprintf(ErrWriter, err.Error()) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %s as int slice value for flag %s: %s", envVal, f.Name, err) } } f.Value = newVal @@ -238,38 +249,96 @@ func (f IntSliceFlag) Apply(set *flag.FlagSet) { } set.Var(f.Value, name, f.Usage) }) + + return nil } -// GetName returns the name of the flag. -func (f IntSliceFlag) GetName() string { - return f.Name -} +// Int64Slice is an opaque type for []int to satisfy flag.Value and flag.Getter +type Int64Slice []int64 -// BoolFlag is a switch that defaults to false -type BoolFlag struct { - Name string - Usage string - EnvVar string - Destination *bool - Hidden bool +// Set parses the value into an integer and appends it to the list of values +func (f *Int64Slice) Set(value string) error { + tmp, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + *f = append(*f, tmp) + return nil } // String returns a readable representation of this value (for usage defaults) -func (f BoolFlag) String() string { - return FlagStringer(f) +func (f *Int64Slice) String() string { + return fmt.Sprintf("%#v", *f) +} + +// Value returns the slice of ints set by this flag +func (f *Int64Slice) Value() []int64 { + return *f +} + +// Get returns the slice of ints set by this flag +func (f *Int64Slice) Get() interface{} { + return *f } // Apply populates the flag given the flag set and environment +// Ignores errors +func (f Int64SliceFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error { + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal, ok := syscall.Getenv(envVar); ok { + newVal := &Int64Slice{} + for _, s := range strings.Split(envVal, ",") { + s = strings.TrimSpace(s) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %s as int64 slice value for flag %s: %s", envVal, f.Name, err) + } + } + f.Value = newVal + break + } + } + } + + eachName(f.Name, func(name string) { + if f.Value == nil { + f.Value = &Int64Slice{} + } + set.Var(f.Value, name, f.Usage) + }) + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors func (f BoolFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f BoolFlag) ApplyWithError(set *flag.FlagSet) error { val := false if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValBool, err := strconv.ParseBool(envVal) - if err == nil { - val = envValBool + if envVal, ok := syscall.Getenv(envVar); ok { + if envVal == "" { + val = false + break } + + envValBool, err := strconv.ParseBool(envVal) + if err != nil { + return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err) + } + + val = envValBool break } } @@ -282,40 +351,35 @@ func (f BoolFlag) Apply(set *flag.FlagSet) { } set.Bool(name, val, f.Usage) }) -} -// GetName returns the name of the flag. -func (f BoolFlag) GetName() string { - return f.Name -} - -// BoolTFlag this represents a boolean flag that is true by default, but can -// still be set to false by --some-flag=false -type BoolTFlag struct { - Name string - Usage string - EnvVar string - Destination *bool - Hidden bool -} - -// String returns a readable representation of this value (for usage defaults) -func (f BoolTFlag) String() string { - return FlagStringer(f) + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f BoolTFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f BoolTFlag) ApplyWithError(set *flag.FlagSet) error { val := true if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValBool, err := strconv.ParseBool(envVal) - if err == nil { - val = envValBool + if envVal, ok := syscall.Getenv(envVar); ok { + if envVal == "" { + val = false break } + + envValBool, err := strconv.ParseBool(envVal) + if err != nil { + return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err) + } + + val = envValBool + break } } } @@ -327,34 +391,22 @@ func (f BoolTFlag) Apply(set *flag.FlagSet) { } set.Bool(name, val, f.Usage) }) -} -// GetName returns the name of the flag. -func (f BoolTFlag) GetName() string { - return f.Name -} - -// StringFlag represents a flag that takes as string value -type StringFlag struct { - Name string - Value string - Usage string - EnvVar string - Destination *string - Hidden bool -} - -// String returns the usage -func (f StringFlag) String() string { - return FlagStringer(f) + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f StringFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f StringFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { f.Value = envVal break } @@ -368,40 +420,28 @@ func (f StringFlag) Apply(set *flag.FlagSet) { } set.String(name, f.Value, f.Usage) }) -} -// GetName returns the name of the flag. -func (f StringFlag) GetName() string { - return f.Name -} - -// IntFlag is a flag that takes an integer -// Errors if the value provided cannot be parsed -type IntFlag struct { - Name string - Value int - Usage string - EnvVar string - Destination *int - Hidden bool -} - -// String returns the usage -func (f IntFlag) String() string { - return FlagStringer(f) + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f IntFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f IntFlag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { envValInt, err := strconv.ParseInt(envVal, 0, 64) - if err == nil { - f.Value = int(envValInt) - break + if err != nil { + return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err) } + f.Value = int(envValInt) + break } } } @@ -413,40 +453,131 @@ func (f IntFlag) Apply(set *flag.FlagSet) { } set.Int(name, f.Value, f.Usage) }) -} -// GetName returns the name of the flag. -func (f IntFlag) GetName() string { - return f.Name -} - -// DurationFlag is a flag that takes a duration specified in Go's duration -// format: https://golang.org/pkg/time/#ParseDuration -type DurationFlag struct { - Name string - Value time.Duration - Usage string - EnvVar string - Destination *time.Duration - Hidden bool -} - -// String returns a readable representation of this value (for usage defaults) -func (f DurationFlag) String() string { - return FlagStringer(f) + return nil } // Apply populates the flag given the flag set and environment -func (f DurationFlag) Apply(set *flag.FlagSet) { +// Ignores errors +func (f Int64Flag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f Int64Flag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - envValDuration, err := time.ParseDuration(envVal) - if err == nil { - f.Value = envValDuration - break + if envVal, ok := syscall.Getenv(envVar); ok { + envValInt, err := strconv.ParseInt(envVal, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err) } + + f.Value = envValInt + break + } + } + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.Int64Var(f.Destination, name, f.Value, f.Usage) + return + } + set.Int64(name, f.Value, f.Usage) + }) + + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f UintFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f UintFlag) ApplyWithError(set *flag.FlagSet) error { + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal, ok := syscall.Getenv(envVar); ok { + envValInt, err := strconv.ParseUint(envVal, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as uint value for flag %s: %s", envVal, f.Name, err) + } + + f.Value = uint(envValInt) + break + } + } + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.UintVar(f.Destination, name, f.Value, f.Usage) + return + } + set.Uint(name, f.Value, f.Usage) + }) + + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f Uint64Flag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f Uint64Flag) ApplyWithError(set *flag.FlagSet) error { + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal, ok := syscall.Getenv(envVar); ok { + envValInt, err := strconv.ParseUint(envVal, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as uint64 value for flag %s: %s", envVal, f.Name, err) + } + + f.Value = uint64(envValInt) + break + } + } + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.Uint64Var(f.Destination, name, f.Value, f.Usage) + return + } + set.Uint64(name, f.Value, f.Usage) + }) + + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f DurationFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f DurationFlag) ApplyWithError(set *flag.FlagSet) error { + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal, ok := syscall.Getenv(envVar); ok { + envValDuration, err := time.ParseDuration(envVal) + if err != nil { + return fmt.Errorf("could not parse %s as duration for flag %s: %s", envVal, f.Name, err) + } + + f.Value = envValDuration + break } } } @@ -458,39 +589,29 @@ func (f DurationFlag) Apply(set *flag.FlagSet) { } set.Duration(name, f.Value, f.Usage) }) -} -// GetName returns the name of the flag. -func (f DurationFlag) GetName() string { - return f.Name -} - -// Float64Flag is a flag that takes an float value -// Errors if the value provided cannot be parsed -type Float64Flag struct { - Name string - Value float64 - Usage string - EnvVar string - Destination *float64 - Hidden bool -} - -// String returns the usage -func (f Float64Flag) String() string { - return FlagStringer(f) + return nil } // Apply populates the flag given the flag set and environment +// Ignores errors func (f Float64Flag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f Float64Flag) ApplyWithError(set *flag.FlagSet) error { if f.EnvVar != "" { for _, envVar := range strings.Split(f.EnvVar, ",") { envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { + if envVal, ok := syscall.Getenv(envVar); ok { envValFloat, err := strconv.ParseFloat(envVal, 10) - if err == nil { - f.Value = float64(envValFloat) + if err != nil { + return fmt.Errorf("could not parse %s as float64 value for flag %s: %s", envVal, f.Name, err) } + + f.Value = float64(envValFloat) + break } } } @@ -502,17 +623,15 @@ func (f Float64Flag) Apply(set *flag.FlagSet) { } set.Float64(name, f.Value, f.Usage) }) -} -// GetName returns the name of the flag. -func (f Float64Flag) GetName() string { - return f.Name + return nil } func visibleFlags(fl []Flag) []Flag { visible := []Flag{} for _, flag := range fl { - if !reflect.ValueOf(flag).FieldByName("Hidden").Bool() { + field := flagValue(flag).FieldByName("Hidden") + if !field.IsValid() || !field.Bool() { visible = append(visible, flag) } } @@ -578,13 +697,24 @@ func withEnvHint(envVar, str string) string { return str + envText } -func stringifyFlag(f Flag) string { +func flagValue(f Flag) reflect.Value { fv := reflect.ValueOf(f) + for fv.Kind() == reflect.Ptr { + fv = reflect.Indirect(fv) + } + return fv +} + +func stringifyFlag(f Flag) string { + fv := flagValue(f) switch f.(type) { case IntSliceFlag: return withEnvHint(fv.FieldByName("EnvVar").String(), stringifyIntSliceFlag(f.(IntSliceFlag))) + case Int64SliceFlag: + return withEnvHint(fv.FieldByName("EnvVar").String(), + stringifyInt64SliceFlag(f.(Int64SliceFlag))) case StringSliceFlag: return withEnvHint(fv.FieldByName("EnvVar").String(), stringifyStringSliceFlag(f.(StringSliceFlag))) @@ -594,9 +724,8 @@ func stringifyFlag(f Flag) string { needsPlaceholder := false defaultValueString := "" - val := fv.FieldByName("Value") - if val.IsValid() { + if val := fv.FieldByName("Value"); val.IsValid() { needsPlaceholder = true defaultValueString = fmt.Sprintf(" (default: %v)", val.Interface()) @@ -630,6 +759,17 @@ func stringifyIntSliceFlag(f IntSliceFlag) string { return stringifySliceFlag(f.Usage, f.Name, defaultVals) } +func stringifyInt64SliceFlag(f Int64SliceFlag) string { + defaultVals := []string{} + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, i := range f.Value.Value() { + defaultVals = append(defaultVals, fmt.Sprintf("%d", i)) + } + } + + return stringifySliceFlag(f.Usage, f.Name, defaultVals) +} + func stringifyStringSliceFlag(f StringSliceFlag) string { defaultVals := []string{} if f.Value != nil && len(f.Value.Value()) > 0 { diff --git a/vendor/github.com/urfave/cli/flag_generated.go b/vendor/github.com/urfave/cli/flag_generated.go new file mode 100644 index 00000000..491b6195 --- /dev/null +++ b/vendor/github.com/urfave/cli/flag_generated.go @@ -0,0 +1,627 @@ +package cli + +import ( + "flag" + "strconv" + "time" +) + +// WARNING: This file is generated! + +// BoolFlag is a flag with type bool +type BoolFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Destination *bool +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f BoolFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f BoolFlag) GetName() string { + return f.Name +} + +// Bool looks up the value of a local BoolFlag, returns +// false if not found +func (c *Context) Bool(name string) bool { + return lookupBool(name, c.flagSet) +} + +// GlobalBool looks up the value of a global BoolFlag, returns +// false if not found +func (c *Context) GlobalBool(name string) bool { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupBool(name, fs) + } + return false +} + +func lookupBool(name string, set *flag.FlagSet) bool { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseBool(f.Value.String()) + if err != nil { + return false + } + return parsed + } + return false +} + +// BoolTFlag is a flag with type bool that is true by default +type BoolTFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Destination *bool +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f BoolTFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f BoolTFlag) GetName() string { + return f.Name +} + +// BoolT looks up the value of a local BoolTFlag, returns +// false if not found +func (c *Context) BoolT(name string) bool { + return lookupBoolT(name, c.flagSet) +} + +// GlobalBoolT looks up the value of a global BoolTFlag, returns +// false if not found +func (c *Context) GlobalBoolT(name string) bool { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupBoolT(name, fs) + } + return false +} + +func lookupBoolT(name string, set *flag.FlagSet) bool { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseBool(f.Value.String()) + if err != nil { + return false + } + return parsed + } + return false +} + +// DurationFlag is a flag with type time.Duration (see https://golang.org/pkg/time/#ParseDuration) +type DurationFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value time.Duration + Destination *time.Duration +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f DurationFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f DurationFlag) GetName() string { + return f.Name +} + +// Duration looks up the value of a local DurationFlag, returns +// 0 if not found +func (c *Context) Duration(name string) time.Duration { + return lookupDuration(name, c.flagSet) +} + +// GlobalDuration looks up the value of a global DurationFlag, returns +// 0 if not found +func (c *Context) GlobalDuration(name string) time.Duration { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupDuration(name, fs) + } + return 0 +} + +func lookupDuration(name string, set *flag.FlagSet) time.Duration { + f := set.Lookup(name) + if f != nil { + parsed, err := time.ParseDuration(f.Value.String()) + if err != nil { + return 0 + } + return parsed + } + return 0 +} + +// Float64Flag is a flag with type float64 +type Float64Flag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value float64 + Destination *float64 +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f Float64Flag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f Float64Flag) GetName() string { + return f.Name +} + +// Float64 looks up the value of a local Float64Flag, returns +// 0 if not found +func (c *Context) Float64(name string) float64 { + return lookupFloat64(name, c.flagSet) +} + +// GlobalFloat64 looks up the value of a global Float64Flag, returns +// 0 if not found +func (c *Context) GlobalFloat64(name string) float64 { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupFloat64(name, fs) + } + return 0 +} + +func lookupFloat64(name string, set *flag.FlagSet) float64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseFloat(f.Value.String(), 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} + +// GenericFlag is a flag with type Generic +type GenericFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value Generic +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f GenericFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f GenericFlag) GetName() string { + return f.Name +} + +// Generic looks up the value of a local GenericFlag, returns +// nil if not found +func (c *Context) Generic(name string) interface{} { + return lookupGeneric(name, c.flagSet) +} + +// GlobalGeneric looks up the value of a global GenericFlag, returns +// nil if not found +func (c *Context) GlobalGeneric(name string) interface{} { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupGeneric(name, fs) + } + return nil +} + +func lookupGeneric(name string, set *flag.FlagSet) interface{} { + f := set.Lookup(name) + if f != nil { + parsed, err := f.Value, error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} + +// Int64Flag is a flag with type int64 +type Int64Flag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value int64 + Destination *int64 +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f Int64Flag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f Int64Flag) GetName() string { + return f.Name +} + +// Int64 looks up the value of a local Int64Flag, returns +// 0 if not found +func (c *Context) Int64(name string) int64 { + return lookupInt64(name, c.flagSet) +} + +// GlobalInt64 looks up the value of a global Int64Flag, returns +// 0 if not found +func (c *Context) GlobalInt64(name string) int64 { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupInt64(name, fs) + } + return 0 +} + +func lookupInt64(name string, set *flag.FlagSet) int64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} + +// IntFlag is a flag with type int +type IntFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value int + Destination *int +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f IntFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f IntFlag) GetName() string { + return f.Name +} + +// Int looks up the value of a local IntFlag, returns +// 0 if not found +func (c *Context) Int(name string) int { + return lookupInt(name, c.flagSet) +} + +// GlobalInt looks up the value of a global IntFlag, returns +// 0 if not found +func (c *Context) GlobalInt(name string) int { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupInt(name, fs) + } + return 0 +} + +func lookupInt(name string, set *flag.FlagSet) int { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return int(parsed) + } + return 0 +} + +// IntSliceFlag is a flag with type *IntSlice +type IntSliceFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value *IntSlice +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f IntSliceFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f IntSliceFlag) GetName() string { + return f.Name +} + +// IntSlice looks up the value of a local IntSliceFlag, returns +// nil if not found +func (c *Context) IntSlice(name string) []int { + return lookupIntSlice(name, c.flagSet) +} + +// GlobalIntSlice looks up the value of a global IntSliceFlag, returns +// nil if not found +func (c *Context) GlobalIntSlice(name string) []int { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupIntSlice(name, fs) + } + return nil +} + +func lookupIntSlice(name string, set *flag.FlagSet) []int { + f := set.Lookup(name) + if f != nil { + parsed, err := (f.Value.(*IntSlice)).Value(), error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} + +// Int64SliceFlag is a flag with type *Int64Slice +type Int64SliceFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value *Int64Slice +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f Int64SliceFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f Int64SliceFlag) GetName() string { + return f.Name +} + +// Int64Slice looks up the value of a local Int64SliceFlag, returns +// nil if not found +func (c *Context) Int64Slice(name string) []int64 { + return lookupInt64Slice(name, c.flagSet) +} + +// GlobalInt64Slice looks up the value of a global Int64SliceFlag, returns +// nil if not found +func (c *Context) GlobalInt64Slice(name string) []int64 { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupInt64Slice(name, fs) + } + return nil +} + +func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { + f := set.Lookup(name) + if f != nil { + parsed, err := (f.Value.(*Int64Slice)).Value(), error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} + +// StringFlag is a flag with type string +type StringFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value string + Destination *string +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f StringFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f StringFlag) GetName() string { + return f.Name +} + +// String looks up the value of a local StringFlag, returns +// "" if not found +func (c *Context) String(name string) string { + return lookupString(name, c.flagSet) +} + +// GlobalString looks up the value of a global StringFlag, returns +// "" if not found +func (c *Context) GlobalString(name string) string { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupString(name, fs) + } + return "" +} + +func lookupString(name string, set *flag.FlagSet) string { + f := set.Lookup(name) + if f != nil { + parsed, err := f.Value.String(), error(nil) + if err != nil { + return "" + } + return parsed + } + return "" +} + +// StringSliceFlag is a flag with type *StringSlice +type StringSliceFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value *StringSlice +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f StringSliceFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f StringSliceFlag) GetName() string { + return f.Name +} + +// StringSlice looks up the value of a local StringSliceFlag, returns +// nil if not found +func (c *Context) StringSlice(name string) []string { + return lookupStringSlice(name, c.flagSet) +} + +// GlobalStringSlice looks up the value of a global StringSliceFlag, returns +// nil if not found +func (c *Context) GlobalStringSlice(name string) []string { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupStringSlice(name, fs) + } + return nil +} + +func lookupStringSlice(name string, set *flag.FlagSet) []string { + f := set.Lookup(name) + if f != nil { + parsed, err := (f.Value.(*StringSlice)).Value(), error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} + +// Uint64Flag is a flag with type uint64 +type Uint64Flag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value uint64 + Destination *uint64 +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f Uint64Flag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f Uint64Flag) GetName() string { + return f.Name +} + +// Uint64 looks up the value of a local Uint64Flag, returns +// 0 if not found +func (c *Context) Uint64(name string) uint64 { + return lookupUint64(name, c.flagSet) +} + +// GlobalUint64 looks up the value of a global Uint64Flag, returns +// 0 if not found +func (c *Context) GlobalUint64(name string) uint64 { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupUint64(name, fs) + } + return 0 +} + +func lookupUint64(name string, set *flag.FlagSet) uint64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} + +// UintFlag is a flag with type uint +type UintFlag struct { + Name string + Usage string + EnvVar string + Hidden bool + Value uint + Destination *uint +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f UintFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f UintFlag) GetName() string { + return f.Name +} + +// Uint looks up the value of a local UintFlag, returns +// 0 if not found +func (c *Context) Uint(name string) uint { + return lookupUint(name, c.flagSet) +} + +// GlobalUint looks up the value of a global UintFlag, returns +// 0 if not found +func (c *Context) GlobalUint(name string) uint { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupUint(name, fs) + } + return 0 +} + +func lookupUint(name string, set *flag.FlagSet) uint { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return uint(parsed) + } + return 0 +} diff --git a/vendor/github.com/urfave/cli/help.go b/vendor/github.com/urfave/cli/help.go index 801d2b16..57ec98d5 100644 --- a/vendor/github.com/urfave/cli/help.go +++ b/vendor/github.com/urfave/cli/help.go @@ -13,27 +13,31 @@ import ( // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. var AppHelpTemplate = `NAME: - {{.Name}} - {{.Usage}} + {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} USAGE: - {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} - {{if .Version}}{{if not .HideVersion}} + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} + VERSION: - {{.Version}} - {{end}}{{end}}{{if len .Authors}} -AUTHOR(S): - {{range .Authors}}{{.}}{{end}} - {{end}}{{if .VisibleCommands}} + {{.Version}}{{end}}{{end}}{{if .Description}} + +DESCRIPTION: + {{.Description}}{{end}}{{if len .Authors}} + +AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: + {{range $index, $author := .Authors}}{{if $index}} + {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} + COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{end}}{{range .VisibleCommands}} - {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{"\t"}}{{.Usage}}{{end}} -{{end}}{{end}}{{if .VisibleFlags}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} + GLOBAL OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}}{{if .Copyright}} + {{range $index, $option := .VisibleFlags}}{{if $index}} + {{end}}{{$option}}{{end}}{{end}}{{if .Copyright}} + COPYRIGHT: - {{.Copyright}} - {{end}} + {{.Copyright}}{{end}} ` // CommandHelpTemplate is the text template for the command help topic. @@ -43,7 +47,7 @@ var CommandHelpTemplate = `NAME: {{.HelpName}} - {{.Usage}} USAGE: - {{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Category}} + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} CATEGORY: {{.Category}}{{end}}{{if .Description}} @@ -60,14 +64,14 @@ OPTIONS: // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. var SubcommandHelpTemplate = `NAME: - {{.HelpName}} - {{.Usage}} + {{.HelpName}} - {{if .Description}}{{.Description}}{{else}}{{.Usage}}{{end}} USAGE: - {{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{end}}{{range .VisibleCommands}} - {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{"\t"}}{{.Usage}}{{end}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}} {{end}}{{if .VisibleFlags}} OPTIONS: {{range .VisibleFlags}}{{.}} @@ -108,17 +112,43 @@ var helpSubcommand = Command{ // Prints help for the App or Command type helpPrinter func(w io.Writer, templ string, data interface{}) +// Prints help for the App or Command with custom template function. +type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{}) + // HelpPrinter is a function that writes the help output. If not set a default // is used. The function signature is: // func(w io.Writer, templ string, data interface{}) var HelpPrinter helpPrinter = printHelp +// HelpPrinterCustom is same as HelpPrinter but +// takes a custom function for template function map. +var HelpPrinterCustom helpPrinterCustom = printHelpCustom + // VersionPrinter prints the version for the App var VersionPrinter = printVersion +// ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. +func ShowAppHelpAndExit(c *Context, exitCode int) { + ShowAppHelp(c) + os.Exit(exitCode) +} + // ShowAppHelp is an action that displays the help. -func ShowAppHelp(c *Context) { - HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) +func ShowAppHelp(c *Context) (err error) { + if c.App.CustomAppHelpTemplate == "" { + HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) + return + } + customAppData := func() map[string]interface{} { + if c.App.ExtraInfo == nil { + return nil + } + return map[string]interface{}{ + "ExtraInfo": c.App.ExtraInfo, + } + } + HelpPrinterCustom(c.App.Writer, c.App.CustomAppHelpTemplate, c.App, customAppData()) + return nil } // DefaultAppComplete prints the list of subcommands as the default app completion method @@ -133,6 +163,12 @@ func DefaultAppComplete(c *Context) { } } +// ShowCommandHelpAndExit - exits with code after showing help +func ShowCommandHelpAndExit(c *Context, command string, code int) { + ShowCommandHelp(c, command) + os.Exit(code) +} + // ShowCommandHelp prints help for the given command func ShowCommandHelp(ctx *Context, command string) error { // show the subcommand help for a command with subcommands @@ -143,7 +179,11 @@ func ShowCommandHelp(ctx *Context, command string) error { for _, c := range ctx.App.Commands { if c.HasName(command) { - HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) + if c.CustomHelpTemplate != "" { + HelpPrinterCustom(ctx.App.Writer, c.CustomHelpTemplate, c, nil) + } else { + HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) + } return nil } } @@ -186,12 +226,17 @@ func ShowCommandCompletions(ctx *Context, command string) { } } -func printHelp(out io.Writer, templ string, data interface{}) { +func printHelpCustom(out io.Writer, templ string, data interface{}, customFunc map[string]interface{}) { funcMap := template.FuncMap{ "join": strings.Join, } + if customFunc != nil { + for key, value := range customFunc { + funcMap[key] = value + } + } - w := tabwriter.NewWriter(out, 0, 8, 1, '\t', 0) + w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0) t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) err := t.Execute(w, data) if err != nil { @@ -205,10 +250,14 @@ func printHelp(out io.Writer, templ string, data interface{}) { w.Flush() } +func printHelp(out io.Writer, templ string, data interface{}) { + printHelpCustom(out, templ, data, nil) +} + func checkVersion(c *Context) bool { found := false - if VersionFlag.Name != "" { - eachName(VersionFlag.Name, func(name string) { + if VersionFlag.GetName() != "" { + eachName(VersionFlag.GetName(), func(name string) { if c.GlobalBool(name) || c.Bool(name) { found = true } @@ -219,8 +268,8 @@ func checkVersion(c *Context) bool { func checkHelp(c *Context) bool { found := false - if HelpFlag.Name != "" { - eachName(HelpFlag.Name, func(name string) { + if HelpFlag.GetName() != "" { + eachName(HelpFlag.GetName(), func(name string) { if c.GlobalBool(name) || c.Bool(name) { found = true } @@ -239,7 +288,7 @@ func checkCommandHelp(c *Context, name string) bool { } func checkSubcommandHelp(c *Context) bool { - if c.GlobalBool("h") || c.GlobalBool("help") { + if c.Bool("h") || c.Bool("help") { ShowSubcommandHelp(c) return true } @@ -247,20 +296,43 @@ func checkSubcommandHelp(c *Context) bool { return false } -func checkCompletions(c *Context) bool { - if (c.GlobalBool(BashCompletionFlag.Name) || c.Bool(BashCompletionFlag.Name)) && c.App.EnableBashCompletion { - ShowCompletions(c) - return true +func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) { + if !a.EnableBashCompletion { + return false, arguments } - return false + pos := len(arguments) - 1 + lastArg := arguments[pos] + + if lastArg != "--"+BashCompletionFlag.GetName() { + return false, arguments + } + + return true, arguments[:pos] +} + +func checkCompletions(c *Context) bool { + if !c.shellComplete { + return false + } + + if args := c.Args(); args.Present() { + name := args.First() + if cmd := c.App.Command(name); cmd != nil { + // let the command handle the completion + return false + } + } + + ShowCompletions(c) + return true } func checkCommandCompletions(c *Context, name string) bool { - if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion { - ShowCommandCompletions(c, name) - return true + if !c.shellComplete { + return false } - return false + ShowCommandCompletions(c, name) + return true } From 3017d87adee45300c26e9dbc56b5761fe40536de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Wed, 11 Jul 2018 22:07:05 +0200 Subject: [PATCH 02/30] Implement an optionalBool, to be used as a cli.GenericFlag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This mirrors the behavior of cli.BoolFlag, but records and explicit "present" indication. Signed-off-by: Miloslav Trmač --- cmd/skopeo/flag.go | 45 +++++++++++++ cmd/skopeo/flag_test.go | 137 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 cmd/skopeo/flag.go create mode 100644 cmd/skopeo/flag_test.go diff --git a/cmd/skopeo/flag.go b/cmd/skopeo/flag.go new file mode 100644 index 00000000..6b4a548d --- /dev/null +++ b/cmd/skopeo/flag.go @@ -0,0 +1,45 @@ +package main + +import ( + "strconv" + + "github.com/urfave/cli" +) + +// optionalBool is a boolean with a separate presence flag. +type optionalBool struct { + present bool + value bool +} + +// optionalBool is a cli.Generic == flag.Value implementation equivalent to +// the one underlying flag.Bool, except that it records whether the flag has been set. +// This is distinct from optionalBool to (pretend to) force callers to use +// newOptionalBool +type optionalBoolValue optionalBool + +func newOptionalBoolValue(p *optionalBool) cli.Generic { + p.present = false + return (*optionalBoolValue)(p) +} + +func (ob *optionalBoolValue) Set(s string) error { + v, err := strconv.ParseBool(s) + if err != nil { + return err + } + ob.value = v + ob.present = true + return nil +} + +func (ob *optionalBoolValue) String() string { + if !ob.present { + return "" // This is, sadly, not round-trip safe: --flag is interpreted as --flag=true + } + return strconv.FormatBool(ob.value) +} + +func (ob *optionalBoolValue) IsBoolFlag() bool { + return true +} diff --git a/cmd/skopeo/flag_test.go b/cmd/skopeo/flag_test.go new file mode 100644 index 00000000..59103b5c --- /dev/null +++ b/cmd/skopeo/flag_test.go @@ -0,0 +1,137 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli" +) + +func TestOptionalBoolSet(t *testing.T) { + for _, c := range []struct { + input string + accepted bool + value bool + }{ + // Valid inputs documented for strconv.ParseBool == flag.BoolVar + {"1", true, true}, + {"t", true, true}, + {"T", true, true}, + {"TRUE", true, true}, + {"true", true, true}, + {"True", true, true}, + {"0", true, false}, + {"f", true, false}, + {"F", true, false}, + {"FALSE", true, false}, + {"false", true, false}, + {"False", true, false}, + // A few invalid inputs + {"", false, false}, + {"yes", false, false}, + {"no", false, false}, + {"2", false, false}, + } { + var ob optionalBool + v := newOptionalBoolValue(&ob) + require.False(t, ob.present) + err := v.Set(c.input) + if c.accepted { + assert.NoError(t, err, c.input) + assert.Equal(t, c.value, ob.value) + } else { + assert.Error(t, err, c.input) + assert.False(t, ob.present) // Just to be extra paranoid. + } + } + + // Nothing actually explicitly says that .Set() is never called when the flag is not present on the command line; + // so, check that it is not being called, at least in the straightforward case (it's not possible to test that it + // is not called in any possible situation). + var globalOB, commandOB optionalBool + actionRun := false + app := cli.NewApp() + app.EnableBashCompletion = true + app.Flags = []cli.Flag{ + cli.GenericFlag{ + Name: "global-OB", + Value: newOptionalBoolValue(&globalOB), + }, + } + app.Commands = []cli.Command{{ + Name: "cmd", + Flags: []cli.Flag{ + cli.GenericFlag{ + Name: "command-OB", + Value: newOptionalBoolValue(&commandOB), + }, + }, + Action: func(*cli.Context) error { + assert.False(t, globalOB.present) + assert.False(t, commandOB.present) + actionRun = true + return nil + }, + }} + err := app.Run([]string{"app", "cmd"}) + require.NoError(t, err) + assert.True(t, actionRun) +} + +func TestOptionalBoolString(t *testing.T) { + for _, c := range []struct { + input optionalBool + expected string + }{ + {optionalBool{present: true, value: true}, "true"}, + {optionalBool{present: true, value: false}, "false"}, + {optionalBool{present: false, value: true}, ""}, + {optionalBool{present: false, value: false}, ""}, + } { + var ob optionalBool + v := newOptionalBoolValue(&ob) + ob = c.input + res := v.String() + assert.Equal(t, c.expected, res) + } +} + +func TestOptionalBoolIsBoolFlag(t *testing.T) { + // IsBoolFlag means that the argument value must either be part of the same argument, with =; + // if there is no =, the value is set to true. + // This differs form other flags, where the argument is required and may be either separated with = or supplied in the next argument. + for _, c := range []struct { + input []string + expectedOB optionalBool + expectedArgs []string + }{ + {[]string{"1", "2"}, optionalBool{present: false}, []string{"1", "2"}}, // Flag not present + {[]string{"--OB=true", "1", "2"}, optionalBool{present: true, value: true}, []string{"1", "2"}}, // --OB=true + {[]string{"--OB=false", "1", "2"}, optionalBool{present: true, value: false}, []string{"1", "2"}}, // --OB=false + {[]string{"--OB", "true", "1", "2"}, optionalBool{present: true, value: true}, []string{"true", "1", "2"}}, // --OB true + {[]string{"--OB", "false", "1", "2"}, optionalBool{present: true, value: true}, []string{"false", "1", "2"}}, // --OB false + } { + var ob optionalBool + actionRun := false + app := cli.NewApp() + app.Commands = []cli.Command{{ + Name: "cmd", + Flags: []cli.Flag{ + cli.GenericFlag{ + Name: "OB", + Value: newOptionalBoolValue(&ob), + }, + }, + Action: func(ctx *cli.Context) error { + assert.Equal(t, c.expectedOB, ob) + assert.Equal(t, c.expectedArgs, ([]string)(ctx.Args())) + actionRun = true + return nil + }, + }} + err := app.Run(append([]string{"app", "cmd"}, c.input...)) + require.NoError(t, err) + assert.True(t, actionRun) + } +} From bc39e4f9b6f73465ede91c370ad8f5d2711eff47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Wed, 11 Jul 2018 22:30:13 +0200 Subject: [PATCH 03/30] Implement an optionalString, to be used as a cli.GenericFlag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This mirrors the behavior of cli.StringFlag, but records an explicit "present" indication. Signed-off-by: Miloslav Trmač --- cmd/skopeo/flag.go | 30 ++++++++++++ cmd/skopeo/flag_test.go | 102 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/cmd/skopeo/flag.go b/cmd/skopeo/flag.go index 6b4a548d..89774ebd 100644 --- a/cmd/skopeo/flag.go +++ b/cmd/skopeo/flag.go @@ -43,3 +43,33 @@ func (ob *optionalBoolValue) String() string { func (ob *optionalBoolValue) IsBoolFlag() bool { return true } + +// optionalString is a string with a separate presence flag. +type optionalString struct { + present bool + value string +} + +// optionalString is a cli.Generic == flag.Value implementation equivalent to +// the one underlying flag.String, except that it records whether the flag has been set. +// This is distinct from optionalString to (pretend to) force callers to use +// newoptionalString +type optionalStringValue optionalString + +func newOptionalStringValue(p *optionalString) cli.Generic { + p.present = false + return (*optionalStringValue)(p) +} + +func (ob *optionalStringValue) Set(s string) error { + ob.value = s + ob.present = true + return nil +} + +func (ob *optionalStringValue) String() string { + if !ob.present { + return "" // This is, sadly, not round-trip safe: --flag= is interpreted as {present:true, value:""} + } + return ob.value +} diff --git a/cmd/skopeo/flag_test.go b/cmd/skopeo/flag_test.go index 59103b5c..4f75effa 100644 --- a/cmd/skopeo/flag_test.go +++ b/cmd/skopeo/flag_test.go @@ -135,3 +135,105 @@ func TestOptionalBoolIsBoolFlag(t *testing.T) { assert.True(t, actionRun) } } + +func TestOptionalStringSet(t *testing.T) { + // Really just a smoke test, but differentiating between not present and empty. + for _, c := range []string{"", "hello"} { + var os optionalString + v := newOptionalStringValue(&os) + require.False(t, os.present) + err := v.Set(c) + assert.NoError(t, err, c) + assert.Equal(t, c, os.value) + } + + // Nothing actually explicitly says that .Set() is never called when the flag is not present on the command line; + // so, check that it is not being called, at least in the straightforward case (it's not possible to test that it + // is not called in any possible situation). + var globalOS, commandOS optionalString + actionRun := false + app := cli.NewApp() + app.EnableBashCompletion = true + app.Flags = []cli.Flag{ + cli.GenericFlag{ + Name: "global-OS", + Value: newOptionalStringValue(&globalOS), + }, + } + app.Commands = []cli.Command{{ + Name: "cmd", + Flags: []cli.Flag{ + cli.GenericFlag{ + Name: "command-OS", + Value: newOptionalStringValue(&commandOS), + }, + }, + Action: func(*cli.Context) error { + assert.False(t, globalOS.present) + assert.False(t, commandOS.present) + actionRun = true + return nil + }, + }} + err := app.Run([]string{"app", "cmd"}) + require.NoError(t, err) + assert.True(t, actionRun) +} + +func TestOptionalStringString(t *testing.T) { + for _, c := range []struct { + input optionalString + expected string + }{ + {optionalString{present: true, value: "hello"}, "hello"}, + {optionalString{present: true, value: ""}, ""}, + {optionalString{present: false, value: "hello"}, ""}, + {optionalString{present: false, value: ""}, ""}, + } { + var os optionalString + v := newOptionalStringValue(&os) + os = c.input + res := v.String() + assert.Equal(t, c.expected, res) + } +} + +func TestOptionalStringIsBoolFlag(t *testing.T) { + // NOTE: optionalStringValue does not implement IsBoolFlag! + // IsBoolFlag means that the argument value must either be part of the same argument, with =; + // if there is no =, the value is set to true. + // This differs form other flags, where the argument is required and may be either separated with = or supplied in the next argument. + for _, c := range []struct { + input []string + expectedOS optionalString + expectedArgs []string + }{ + {[]string{"1", "2"}, optionalString{present: false}, []string{"1", "2"}}, // Flag not present + {[]string{"--OS=hello", "1", "2"}, optionalString{present: true, value: "hello"}, []string{"1", "2"}}, // --OS=true + {[]string{"--OS=", "1", "2"}, optionalString{present: true, value: ""}, []string{"1", "2"}}, // --OS=false + {[]string{"--OS", "hello", "1", "2"}, optionalString{present: true, value: "hello"}, []string{"1", "2"}}, // --OS true + {[]string{"--OS", "", "1", "2"}, optionalString{present: true, value: ""}, []string{"1", "2"}}, // --OS false + } { + var os optionalString + actionRun := false + app := cli.NewApp() + app.Commands = []cli.Command{{ + Name: "cmd", + Flags: []cli.Flag{ + cli.GenericFlag{ + Name: "OS", + Value: newOptionalStringValue(&os), + }, + }, + Action: func(ctx *cli.Context) error { + assert.Equal(t, c.expectedOS, os) + assert.Equal(t, c.expectedArgs, ([]string)(ctx.Args())) + actionRun = true + return nil + }, + }} + err := app.Run(append([]string{"app", "cmd"}, c.input...)) + require.NoError(t, err) + assert.True(t, actionRun) + } +} From 8ee3ead7432fb79d5f4b04ad5fcdbca31458db0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Jul 2018 01:40:21 +0200 Subject: [PATCH 04/30] Create an "options" structure for each command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a big diff, but it really only replaces a few global variables with functions returning a structure. The ultimate goal of this patch set is to replace option handling using > cli.StringFlag{Name:"foo", ...} > ... > func somethingHandler(c *cli.Context) error { > c.String("foo") > } where the declaration and usage are connected only using a string constant, and it's difficult to notice that one or the other is missing or that the types don't match, by > struct somethingOptions { > foo string > } > ... > cli.StringFlag{Name:"foo", Destination:&foo} > ... > func (opts *somethingOptions) run(c *cli.Context) error { > opts.foo > } As a first step, this commit ONLY introduces the *Options structures, but for now empty; nothing changes in the existing implementations. So, we go from > func somethingHandler(c *cli.Context error {...} > > var somethingCmd = cli.Command { > ... > Action: somethingHandler > } to > type somethingOptions struct{ > } // empty for now > > func somethingCmd() cli.Command { > opts := somethingOptions{} > return cli.Command { > ... // unchanged > Action: opts.run > } > } > > func (opts *somethingOptions) run(c *cli.context) error {...} // unchanged Using the struct type has also made it possible to place the definition of cli.Command in front of the actual command handler, so do that for better readability. In a few cases this also broke out an in-line lambda in the Action: field into a separate opts.run method. Again, nothing else has changed. Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 200 ++++++++++++++++++++------------------- cmd/skopeo/delete.go | 78 +++++++++------- cmd/skopeo/inspect.go | 208 +++++++++++++++++++++-------------------- cmd/skopeo/layers.go | 198 ++++++++++++++++++++------------------- cmd/skopeo/main.go | 16 ++-- cmd/skopeo/manifest.go | 22 +++-- cmd/skopeo/signing.go | 84 ++++++++++------- 7 files changed, 429 insertions(+), 377 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index be8e9068..9bffa0af 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -31,7 +31,109 @@ func contextsFromGlobalOptions(c *cli.Context) (*types.SystemContext, *types.Sys return sourceCtx, destinationCtx, nil } -func copyHandler(c *cli.Context) error { +type copyOptions struct { +} + +func copyCmd() cli.Command { + opts := copyOptions{} + return cli.Command{ + Name: "copy", + Usage: "Copy an IMAGE-NAME from one location to another", + Description: fmt.Sprintf(` + + Container "IMAGE-NAME" uses a "transport":"details" format. + + Supported transports: + %s + + See skopeo(1) section "IMAGE NAMES" for the expected format + `, strings.Join(transports.ListNames(), ", ")), + ArgsUsage: "SOURCE-IMAGE DESTINATION-IMAGE", + Action: opts.run, + // FIXME: Do we need to namespace the GPG aspect? + Flags: []cli.Flag{ + cli.StringSliceFlag{ + Name: "additional-tag", + Usage: "additional tags (supports docker-archive)", + }, + cli.StringFlag{ + Name: "authfile", + Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", + }, + cli.BoolFlag{ + Name: "remove-signatures", + Usage: "Do not copy signatures from SOURCE-IMAGE", + }, + cli.StringFlag{ + Name: "sign-by", + Usage: "Sign the image using a GPG key with the specified `FINGERPRINT`", + }, + cli.StringFlag{ + Name: "src-creds, screds", + Value: "", + Usage: "Use `USERNAME[:PASSWORD]` for accessing the source registry", + }, + cli.StringFlag{ + Name: "dest-creds, dcreds", + Value: "", + Usage: "Use `USERNAME[:PASSWORD]` for accessing the destination registry", + }, + cli.StringFlag{ + Name: "src-cert-dir", + Value: "", + Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the source registry or daemon", + }, + cli.BoolTFlag{ + Name: "src-tls-verify", + Usage: "require HTTPS and verify certificates when talking to the container source registry or daemon (defaults to true)", + }, + cli.StringFlag{ + Name: "dest-cert-dir", + Value: "", + Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the destination registry or daemon", + }, + cli.BoolTFlag{ + Name: "dest-tls-verify", + Usage: "require HTTPS and verify certificates when talking to the container destination registry or daemon (defaults to true)", + }, + cli.StringFlag{ + Name: "dest-ostree-tmp-dir", + Value: "", + Usage: "`DIRECTORY` to use for OSTree temporary files", + }, + cli.StringFlag{ + Name: "src-shared-blob-dir", + Value: "", + Usage: "`DIRECTORY` to use to fetch retrieved blobs (OCI layout sources only)", + }, + cli.StringFlag{ + Name: "dest-shared-blob-dir", + Value: "", + Usage: "`DIRECTORY` to use to store retrieved blobs (OCI layout destinations only)", + }, + cli.StringFlag{ + Name: "format, f", + Usage: "`MANIFEST TYPE` (oci, v2s1, or v2s2) to use when saving image to directory using the 'dir:' transport (default is manifest type of source)", + }, + cli.BoolFlag{ + Name: "dest-compress", + Usage: "Compress tarball image layers when saving to directory using the 'dir' transport. (default is same compression type as source)", + }, + cli.StringFlag{ + Name: "src-daemon-host", + Value: "", + Usage: "use docker daemon host at `HOST` (docker-daemon sources only)", + }, + cli.StringFlag{ + Name: "dest-daemon-host", + Value: "", + Usage: "use docker daemon host at `HOST` (docker-daemon destinations only)", + }, + }, + } +} + +func (opts *copyOptions) run(c *cli.Context) error { if len(c.Args()) != 2 { cli.ShowCommandHelp(c, "copy") return errors.New("Exactly two arguments expected") @@ -100,99 +202,3 @@ func copyHandler(c *cli.Context) error { }) return err } - -var copyCmd = cli.Command{ - Name: "copy", - Usage: "Copy an IMAGE-NAME from one location to another", - Description: fmt.Sprintf(` - - Container "IMAGE-NAME" uses a "transport":"details" format. - - Supported transports: - %s - - See skopeo(1) section "IMAGE NAMES" for the expected format - `, strings.Join(transports.ListNames(), ", ")), - ArgsUsage: "SOURCE-IMAGE DESTINATION-IMAGE", - Action: copyHandler, - // FIXME: Do we need to namespace the GPG aspect? - Flags: []cli.Flag{ - cli.StringSliceFlag{ - Name: "additional-tag", - Usage: "additional tags (supports docker-archive)", - }, - cli.StringFlag{ - Name: "authfile", - Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", - }, - cli.BoolFlag{ - Name: "remove-signatures", - Usage: "Do not copy signatures from SOURCE-IMAGE", - }, - cli.StringFlag{ - Name: "sign-by", - Usage: "Sign the image using a GPG key with the specified `FINGERPRINT`", - }, - cli.StringFlag{ - Name: "src-creds, screds", - Value: "", - Usage: "Use `USERNAME[:PASSWORD]` for accessing the source registry", - }, - cli.StringFlag{ - Name: "dest-creds, dcreds", - Value: "", - Usage: "Use `USERNAME[:PASSWORD]` for accessing the destination registry", - }, - cli.StringFlag{ - Name: "src-cert-dir", - Value: "", - Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the source registry or daemon", - }, - cli.BoolTFlag{ - Name: "src-tls-verify", - Usage: "require HTTPS and verify certificates when talking to the container source registry or daemon (defaults to true)", - }, - cli.StringFlag{ - Name: "dest-cert-dir", - Value: "", - Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the destination registry or daemon", - }, - cli.BoolTFlag{ - Name: "dest-tls-verify", - Usage: "require HTTPS and verify certificates when talking to the container destination registry or daemon (defaults to true)", - }, - cli.StringFlag{ - Name: "dest-ostree-tmp-dir", - Value: "", - Usage: "`DIRECTORY` to use for OSTree temporary files", - }, - cli.StringFlag{ - Name: "src-shared-blob-dir", - Value: "", - Usage: "`DIRECTORY` to use to fetch retrieved blobs (OCI layout sources only)", - }, - cli.StringFlag{ - Name: "dest-shared-blob-dir", - Value: "", - Usage: "`DIRECTORY` to use to store retrieved blobs (OCI layout destinations only)", - }, - cli.StringFlag{ - Name: "format, f", - Usage: "`MANIFEST TYPE` (oci, v2s1, or v2s2) to use when saving image to directory using the 'dir:' transport (default is manifest type of source)", - }, - cli.BoolFlag{ - Name: "dest-compress", - Usage: "Compress tarball image layers when saving to directory using the 'dir' transport. (default is same compression type as source)", - }, - cli.StringFlag{ - Name: "src-daemon-host", - Value: "", - Usage: "use docker daemon host at `HOST` (docker-daemon sources only)", - }, - cli.StringFlag{ - Name: "dest-daemon-host", - Value: "", - Usage: "use docker daemon host at `HOST` (docker-daemon destinations only)", - }, - }, -} diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go index 5cc7e203..4479f0a2 100644 --- a/cmd/skopeo/delete.go +++ b/cmd/skopeo/delete.go @@ -10,7 +10,48 @@ import ( "github.com/urfave/cli" ) -func deleteHandler(c *cli.Context) error { +type deleteOptions struct { +} + +func deleteCmd() cli.Command { + opts := deleteOptions{} + return cli.Command{ + Name: "delete", + Usage: "Delete image IMAGE-NAME", + Description: fmt.Sprintf(` + Delete an "IMAGE_NAME" from a transport + + Supported transports: + %s + + See skopeo(1) section "IMAGE NAMES" for the expected format + `, strings.Join(transports.ListNames(), ", ")), + ArgsUsage: "IMAGE-NAME", + Action: opts.run, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "authfile", + Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", + }, + cli.StringFlag{ + Name: "creds", + Value: "", + Usage: "Use `USERNAME[:PASSWORD]` for accessing the registry", + }, + cli.StringFlag{ + Name: "cert-dir", + Value: "", + Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the registry", + }, + cli.BoolTFlag{ + Name: "tls-verify", + Usage: "require HTTPS and verify certificates when talking to container registries (defaults to true)", + }, + }, + } +} + +func (opts *deleteOptions) run(c *cli.Context) error { if len(c.Args()) != 1 { return errors.New("Usage: delete imageReference") } @@ -29,38 +70,3 @@ func deleteHandler(c *cli.Context) error { defer cancel() return ref.DeleteImage(ctx, sys) } - -var deleteCmd = cli.Command{ - Name: "delete", - Usage: "Delete image IMAGE-NAME", - Description: fmt.Sprintf(` - Delete an "IMAGE_NAME" from a transport - - Supported transports: - %s - - See skopeo(1) section "IMAGE NAMES" for the expected format - `, strings.Join(transports.ListNames(), ", ")), - ArgsUsage: "IMAGE-NAME", - Action: deleteHandler, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "authfile", - Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", - }, - cli.StringFlag{ - Name: "creds", - Value: "", - Usage: "Use `USERNAME[:PASSWORD]` for accessing the registry", - }, - cli.StringFlag{ - Name: "cert-dir", - Value: "", - Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the registry", - }, - cli.BoolTFlag{ - Name: "tls-verify", - Usage: "require HTTPS and verify certificates when talking to container registries (defaults to true)", - }, - }, -} diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 2238e436..ab1b0781 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -29,10 +29,15 @@ type inspectOutput struct { Layers []string } -var inspectCmd = cli.Command{ - Name: "inspect", - Usage: "Inspect image IMAGE-NAME", - Description: fmt.Sprintf(` +type inspectOptions struct { +} + +func inspectCmd() cli.Command { + opts := inspectOptions{} + return cli.Command{ + Name: "inspect", + Usage: "Inspect image IMAGE-NAME", + Description: fmt.Sprintf(` Return low-level information about "IMAGE-NAME" in a registry/transport Supported transports: @@ -40,101 +45,104 @@ var inspectCmd = cli.Command{ See skopeo(1) section "IMAGE NAMES" for the expected format `, strings.Join(transports.ListNames(), ", ")), - ArgsUsage: "IMAGE-NAME", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "authfile", - Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", + ArgsUsage: "IMAGE-NAME", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "authfile", + Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", + }, + cli.StringFlag{ + Name: "cert-dir", + Value: "", + Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the registry", + }, + cli.BoolTFlag{ + Name: "tls-verify", + Usage: "require HTTPS and verify certificates when talking to container registries (defaults to true)", + }, + cli.BoolFlag{ + Name: "raw", + Usage: "output raw manifest", + }, + cli.StringFlag{ + Name: "creds", + Value: "", + Usage: "Use `USERNAME[:PASSWORD]` for accessing the registry", + }, }, - cli.StringFlag{ - Name: "cert-dir", - Value: "", - Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the registry", - }, - cli.BoolTFlag{ - Name: "tls-verify", - Usage: "require HTTPS and verify certificates when talking to container registries (defaults to true)", - }, - cli.BoolFlag{ - Name: "raw", - Usage: "output raw manifest", - }, - cli.StringFlag{ - Name: "creds", - Value: "", - Usage: "Use `USERNAME[:PASSWORD]` for accessing the registry", - }, - }, - Action: func(c *cli.Context) (retErr error) { - ctx, cancel := commandTimeoutContextFromGlobalOptions(c) - defer cancel() - - img, err := parseImage(ctx, c) - if err != nil { - return err - } - - defer func() { - if err := img.Close(); err != nil { - retErr = errors.Wrapf(retErr, fmt.Sprintf("(could not close image: %v) ", err)) - } - }() - - rawManifest, _, err := img.Manifest(ctx) - if err != nil { - return err - } - if c.Bool("raw") { - _, err := c.App.Writer.Write(rawManifest) - if err != nil { - return fmt.Errorf("Error writing manifest to standard output: %v", err) - } - return nil - } - imgInspect, err := img.Inspect(ctx) - if err != nil { - return err - } - outputData := inspectOutput{ - Name: "", // Set below if DockerReference() is known - Tag: imgInspect.Tag, - // Digest is set below. - RepoTags: []string{}, // Possibly overriden for docker.Transport. - Created: imgInspect.Created, - DockerVersion: imgInspect.DockerVersion, - Labels: imgInspect.Labels, - Architecture: imgInspect.Architecture, - Os: imgInspect.Os, - Layers: imgInspect.Layers, - } - outputData.Digest, err = manifest.Digest(rawManifest) - if err != nil { - return fmt.Errorf("Error computing manifest digest: %v", err) - } - if dockerRef := img.Reference().DockerReference(); dockerRef != nil { - outputData.Name = dockerRef.Name() - } - if img.Reference().Transport() == docker.Transport { - sys, err := contextFromGlobalOptions(c, "") - if err != nil { - return err - } - outputData.RepoTags, err = docker.GetRepositoryTags(ctx, sys, img.Reference()) - if err != nil { - // some registries may decide to block the "list all tags" endpoint - // gracefully allow the inspect to continue in this case. Currently - // the IBM Bluemix container registry has this restriction. - if !strings.Contains(err.Error(), "401") { - return fmt.Errorf("Error determining repository tags: %v", err) - } - logrus.Warnf("Registry disallows tag list retrieval; skipping") - } - } - out, err := json.MarshalIndent(outputData, "", " ") - if err != nil { - return err - } - fmt.Fprintln(c.App.Writer, string(out)) - return nil - }, + Action: opts.run, + } +} + +func (opts *inspectOptions) run(c *cli.Context) (retErr error) { + ctx, cancel := commandTimeoutContextFromGlobalOptions(c) + defer cancel() + + img, err := parseImage(ctx, c) + if err != nil { + return err + } + + defer func() { + if err := img.Close(); err != nil { + retErr = errors.Wrapf(retErr, fmt.Sprintf("(could not close image: %v) ", err)) + } + }() + + rawManifest, _, err := img.Manifest(ctx) + if err != nil { + return err + } + if c.Bool("raw") { + _, err := c.App.Writer.Write(rawManifest) + if err != nil { + return fmt.Errorf("Error writing manifest to standard output: %v", err) + } + return nil + } + imgInspect, err := img.Inspect(ctx) + if err != nil { + return err + } + outputData := inspectOutput{ + Name: "", // Set below if DockerReference() is known + Tag: imgInspect.Tag, + // Digest is set below. + RepoTags: []string{}, // Possibly overriden for docker.Transport. + Created: imgInspect.Created, + DockerVersion: imgInspect.DockerVersion, + Labels: imgInspect.Labels, + Architecture: imgInspect.Architecture, + Os: imgInspect.Os, + Layers: imgInspect.Layers, + } + outputData.Digest, err = manifest.Digest(rawManifest) + if err != nil { + return fmt.Errorf("Error computing manifest digest: %v", err) + } + if dockerRef := img.Reference().DockerReference(); dockerRef != nil { + outputData.Name = dockerRef.Name() + } + if img.Reference().Transport() == docker.Transport { + sys, err := contextFromGlobalOptions(c, "") + if err != nil { + return err + } + outputData.RepoTags, err = docker.GetRepositoryTags(ctx, sys, img.Reference()) + if err != nil { + // some registries may decide to block the "list all tags" endpoint + // gracefully allow the inspect to continue in this case. Currently + // the IBM Bluemix container registry has this restriction. + if !strings.Contains(err.Error(), "401") { + return fmt.Errorf("Error determining repository tags: %v", err) + } + logrus.Warnf("Registry disallows tag list retrieval; skipping") + } + } + out, err := json.MarshalIndent(outputData, "", " ") + if err != nil { + return err + } + fmt.Fprintln(c.App.Writer, string(out)) + return nil } diff --git a/cmd/skopeo/layers.go b/cmd/skopeo/layers.go index ec95c795..a5b76ae6 100644 --- a/cmd/skopeo/layers.go +++ b/cmd/skopeo/layers.go @@ -15,114 +15,122 @@ import ( "github.com/urfave/cli" ) -var layersCmd = cli.Command{ - Name: "layers", - Usage: "Get layers of IMAGE-NAME", - ArgsUsage: "IMAGE-NAME [LAYER...]", - Hidden: true, - Action: func(c *cli.Context) (retErr error) { - fmt.Fprintln(os.Stderr, `DEPRECATED: skopeo layers is deprecated in favor of skopeo copy`) - if c.NArg() == 0 { - return errors.New("Usage: layers imageReference [layer...]") +type layersOptions struct { +} + +func layersCmd() cli.Command { + opts := &layersOptions{} + return cli.Command{ + Name: "layers", + Usage: "Get layers of IMAGE-NAME", + ArgsUsage: "IMAGE-NAME [LAYER...]", + Hidden: true, + Action: opts.run, + } +} + +func (opts *layersOptions) run(c *cli.Context) (retErr error) { + fmt.Fprintln(os.Stderr, `DEPRECATED: skopeo layers is deprecated in favor of skopeo copy`) + if c.NArg() == 0 { + return errors.New("Usage: layers imageReference [layer...]") + } + + ctx, cancel := commandTimeoutContextFromGlobalOptions(c) + defer cancel() + + sys, err := contextFromGlobalOptions(c, "") + if err != nil { + return err + } + cache := blobinfocache.DefaultCache(sys) + rawSource, err := parseImageSource(ctx, c, c.Args()[0]) + if err != nil { + return err + } + src, err := image.FromSource(ctx, sys, rawSource) + if err != nil { + if closeErr := rawSource.Close(); closeErr != nil { + return errors.Wrapf(err, " (close error: %v)", closeErr) } - ctx, cancel := commandTimeoutContextFromGlobalOptions(c) - defer cancel() + return err + } + defer func() { + if err := src.Close(); err != nil { + retErr = errors.Wrapf(retErr, " (close error: %v)", err) + } + }() - sys, err := contextFromGlobalOptions(c, "") + type blobDigest struct { + digest digest.Digest + isConfig bool + } + var blobDigests []blobDigest + for _, dString := range c.Args().Tail() { + if !strings.HasPrefix(dString, "sha256:") { + dString = "sha256:" + dString + } + d, err := digest.Parse(dString) if err != nil { return err } - cache := blobinfocache.DefaultCache(sys) - rawSource, err := parseImageSource(ctx, c, c.Args()[0]) + blobDigests = append(blobDigests, blobDigest{digest: d, isConfig: false}) + } + + if len(blobDigests) == 0 { + layers := src.LayerInfos() + seenLayers := map[digest.Digest]struct{}{} + for _, info := range layers { + if _, ok := seenLayers[info.Digest]; !ok { + blobDigests = append(blobDigests, blobDigest{digest: info.Digest, isConfig: false}) + seenLayers[info.Digest] = struct{}{} + } + } + configInfo := src.ConfigInfo() + if configInfo.Digest != "" { + blobDigests = append(blobDigests, blobDigest{digest: configInfo.Digest, isConfig: true}) + } + } + + tmpDir, err := ioutil.TempDir(".", "layers-") + if err != nil { + return err + } + tmpDirRef, err := directory.NewReference(tmpDir) + if err != nil { + return err + } + dest, err := tmpDirRef.NewImageDestination(ctx, nil) + if err != nil { + return err + } + + defer func() { + if err := dest.Close(); err != nil { + retErr = errors.Wrapf(retErr, " (close error: %v)", err) + } + }() + + for _, bd := range blobDigests { + r, blobSize, err := rawSource.GetBlob(ctx, types.BlobInfo{Digest: bd.digest, Size: -1}, cache) if err != nil { return err } - src, err := image.FromSource(ctx, sys, rawSource) - if err != nil { - if closeErr := rawSource.Close(); closeErr != nil { + if _, err := dest.PutBlob(ctx, r, types.BlobInfo{Digest: bd.digest, Size: blobSize}, cache, bd.isConfig); err != nil { + if closeErr := r.Close(); closeErr != nil { return errors.Wrapf(err, " (close error: %v)", closeErr) } - return err } - defer func() { - if err := src.Close(); err != nil { - retErr = errors.Wrapf(retErr, " (close error: %v)", err) - } - }() + } - type blobDigest struct { - digest digest.Digest - isConfig bool - } - var blobDigests []blobDigest - for _, dString := range c.Args().Tail() { - if !strings.HasPrefix(dString, "sha256:") { - dString = "sha256:" + dString - } - d, err := digest.Parse(dString) - if err != nil { - return err - } - blobDigests = append(blobDigests, blobDigest{digest: d, isConfig: false}) - } + manifest, _, err := src.Manifest(ctx) + if err != nil { + return err + } + if err := dest.PutManifest(ctx, manifest); err != nil { + return err + } - if len(blobDigests) == 0 { - layers := src.LayerInfos() - seenLayers := map[digest.Digest]struct{}{} - for _, info := range layers { - if _, ok := seenLayers[info.Digest]; !ok { - blobDigests = append(blobDigests, blobDigest{digest: info.Digest, isConfig: false}) - seenLayers[info.Digest] = struct{}{} - } - } - configInfo := src.ConfigInfo() - if configInfo.Digest != "" { - blobDigests = append(blobDigests, blobDigest{digest: configInfo.Digest, isConfig: true}) - } - } - - tmpDir, err := ioutil.TempDir(".", "layers-") - if err != nil { - return err - } - tmpDirRef, err := directory.NewReference(tmpDir) - if err != nil { - return err - } - dest, err := tmpDirRef.NewImageDestination(ctx, nil) - if err != nil { - return err - } - - defer func() { - if err := dest.Close(); err != nil { - retErr = errors.Wrapf(retErr, " (close error: %v)", err) - } - }() - - for _, bd := range blobDigests { - r, blobSize, err := rawSource.GetBlob(ctx, types.BlobInfo{Digest: bd.digest, Size: -1}, cache) - if err != nil { - return err - } - if _, err := dest.PutBlob(ctx, r, types.BlobInfo{Digest: bd.digest, Size: blobSize}, cache, bd.isConfig); err != nil { - if closeErr := r.Close(); closeErr != nil { - return errors.Wrapf(err, " (close error: %v)", closeErr) - } - return err - } - } - - manifest, _, err := src.Manifest(ctx) - if err != nil { - return err - } - if err := dest.PutManifest(ctx, manifest); err != nil { - return err - } - - return dest.Commit(ctx) - }, + return dest.Commit(ctx) } diff --git a/cmd/skopeo/main.go b/cmd/skopeo/main.go index 78fa2af3..6a2fd560 100644 --- a/cmd/skopeo/main.go +++ b/cmd/skopeo/main.go @@ -75,14 +75,14 @@ func createApp() *cli.App { return nil } app.Commands = []cli.Command{ - copyCmd, - inspectCmd, - layersCmd, - deleteCmd, - manifestDigestCmd, - standaloneSignCmd, - standaloneVerifyCmd, - untrustedSignatureDumpCmd, + copyCmd(), + inspectCmd(), + layersCmd(), + deleteCmd(), + manifestDigestCmd(), + standaloneSignCmd(), + standaloneVerifyCmd(), + untrustedSignatureDumpCmd(), } return app } diff --git a/cmd/skopeo/manifest.go b/cmd/skopeo/manifest.go index a04f5e21..177fefe5 100644 --- a/cmd/skopeo/manifest.go +++ b/cmd/skopeo/manifest.go @@ -9,7 +9,20 @@ import ( "github.com/urfave/cli" ) -func manifestDigest(context *cli.Context) error { +type manifestDigestOptions struct { +} + +func manifestDigestCmd() cli.Command { + opts := manifestDigestOptions{} + return cli.Command{ + Name: "manifest-digest", + Usage: "Compute a manifest digest of a file", + ArgsUsage: "MANIFEST", + Action: opts.run, + } +} + +func (opts *manifestDigestOptions) run(context *cli.Context) error { if len(context.Args()) != 1 { return errors.New("Usage: skopeo manifest-digest manifest") } @@ -26,10 +39,3 @@ func manifestDigest(context *cli.Context) error { fmt.Fprintf(context.App.Writer, "%s\n", digest) return nil } - -var manifestDigestCmd = cli.Command{ - Name: "manifest-digest", - Usage: "Compute a manifest digest of a file", - ArgsUsage: "MANIFEST", - Action: manifestDigest, -} diff --git a/cmd/skopeo/signing.go b/cmd/skopeo/signing.go index 500fedd5..fd0332e3 100644 --- a/cmd/skopeo/signing.go +++ b/cmd/skopeo/signing.go @@ -10,7 +10,26 @@ import ( "github.com/urfave/cli" ) -func standaloneSign(c *cli.Context) error { +type standaloneSignOptions struct { +} + +func standaloneSignCmd() cli.Command { + opts := standaloneSignOptions{} + return cli.Command{ + Name: "standalone-sign", + Usage: "Create a signature using local files", + ArgsUsage: "MANIFEST DOCKER-REFERENCE KEY-FINGERPRINT", + Action: opts.run, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "output, o", + Usage: "output the signature to `SIGNATURE`", + }, + }, + } +} + +func (opts *standaloneSignOptions) run(c *cli.Context) error { outputFile := c.String("output") if len(c.Args()) != 3 || outputFile == "" { return errors.New("Usage: skopeo standalone-sign manifest docker-reference key-fingerprint -o signature") @@ -40,20 +59,20 @@ func standaloneSign(c *cli.Context) error { return nil } -var standaloneSignCmd = cli.Command{ - Name: "standalone-sign", - Usage: "Create a signature using local files", - ArgsUsage: "MANIFEST DOCKER-REFERENCE KEY-FINGERPRINT", - Action: standaloneSign, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "output, o", - Usage: "output the signature to `SIGNATURE`", - }, - }, +type standaloneVerifyOptions struct { } -func standaloneVerify(c *cli.Context) error { +func standaloneVerifyCmd() cli.Command { + opts := standaloneVerifyOptions{} + return cli.Command{ + Name: "standalone-verify", + Usage: "Verify a signature using local files", + ArgsUsage: "MANIFEST DOCKER-REFERENCE KEY-FINGERPRINT SIGNATURE", + Action: opts.run, + } +} + +func (opts *standaloneVerifyOptions) run(c *cli.Context) error { if len(c.Args()) != 4 { return errors.New("Usage: skopeo standalone-verify manifest docker-reference key-fingerprint signature") } @@ -85,14 +104,27 @@ func standaloneVerify(c *cli.Context) error { return nil } -var standaloneVerifyCmd = cli.Command{ - Name: "standalone-verify", - Usage: "Verify a signature using local files", - ArgsUsage: "MANIFEST DOCKER-REFERENCE KEY-FINGERPRINT SIGNATURE", - Action: standaloneVerify, +// WARNING: Do not use the contents of this for ANY security decisions, +// and be VERY CAREFUL about showing this information to humans in any way which suggest that these values “are probably” reliable. +// There is NO REASON to expect the values to be correct, or not intentionally misleading +// (including things like “✅ Verified by $authority”) +// +// The subcommand is undocumented, and it may be renamed or entirely disappear in the future. +type untrustedSignatureDumpOptions struct { } -func untrustedSignatureDump(c *cli.Context) error { +func untrustedSignatureDumpCmd() cli.Command { + opts := untrustedSignatureDumpOptions{} + return cli.Command{ + Name: "untrusted-signature-dump-without-verification", + Usage: "Dump contents of a signature WITHOUT VERIFYING IT", + ArgsUsage: "SIGNATURE", + Hidden: true, + Action: opts.run, + } +} + +func (opts *untrustedSignatureDumpOptions) run(c *cli.Context) error { if len(c.Args()) != 1 { return errors.New("Usage: skopeo untrusted-signature-dump-without-verification signature") } @@ -114,17 +146,3 @@ func untrustedSignatureDump(c *cli.Context) error { fmt.Fprintln(c.App.Writer, string(untrustedOut)) return nil } - -// WARNING: Do not use the contents of this for ANY security decisions, -// and be VERY CAREFUL about showing this information to humans in any way which suggest that these values “are probably” reliable. -// There is NO REASON to expect the values to be correct, or not intentionally misleading -// (including things like “✅ Verified by $authority”) -// -// The subcommand is undocumented, and it may be renamed or entirely disappear in the future. -var untrustedSignatureDumpCmd = cli.Command{ - Name: "untrusted-signature-dump-without-verification", - Usage: "Dump contents of a signature WITHOUT VERIFYING IT", - ArgsUsage: "SIGNATURE", - Hidden: true, - Action: untrustedSignatureDump, -} From 59117e6e3d3407a5449df6f40d924c10e9bfaa78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Jul 2018 02:40:39 +0200 Subject: [PATCH 05/30] Fix a typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 9bffa0af..69d7503e 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -171,7 +171,7 @@ func (opts *copyOptions) run(c *cli.Context) error { case "v2s2": manifestType = manifest.DockerV2Schema2MediaType default: - return fmt.Errorf("unknown format %q. Choose on of the supported formats: 'oci', 'v2s1', or 'v2s2'", c.String("format")) + return fmt.Errorf("unknown format %q. Choose one of the supported formats: 'oci', 'v2s1', or 'v2s2'", c.String("format")) } } From 485a7aa330ade7da57c545b72c2be2da64f7d1f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Jul 2018 02:33:47 +0200 Subject: [PATCH 06/30] Use the *Options structures for command-specific options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use Destionation: &opts.flag in the flag definition instead of c.String("flag-name") and the like in the hadler and matching only by strings. Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 50 +++++++++++++++++++++++-------------------- cmd/skopeo/inspect.go | 8 ++++--- cmd/skopeo/signing.go | 13 +++++------ 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 69d7503e..23da3919 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -32,6 +32,10 @@ func contextsFromGlobalOptions(c *cli.Context) (*types.SystemContext, *types.Sys } type copyOptions struct { + additionalTags cli.StringSlice // For docker-archive: destinations, in addition to the name:tag specified as destination, also add these + removeSignatures bool // Do not copy signatures from the source image + signByFingerprint string // Sign the image using a GPG key with the specified fingerprint + format optionalString // Force conversion of the image to a specified format } func copyCmd() cli.Command { @@ -55,18 +59,21 @@ func copyCmd() cli.Command { cli.StringSliceFlag{ Name: "additional-tag", Usage: "additional tags (supports docker-archive)", + Value: &opts.additionalTags, // Surprisingly StringSliceFlag does not support Destination:, but modifies Value: in place. }, cli.StringFlag{ Name: "authfile", Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", }, cli.BoolFlag{ - Name: "remove-signatures", - Usage: "Do not copy signatures from SOURCE-IMAGE", + Name: "remove-signatures", + Usage: "Do not copy signatures from SOURCE-IMAGE", + Destination: &opts.removeSignatures, }, cli.StringFlag{ - Name: "sign-by", - Usage: "Sign the image using a GPG key with the specified `FINGERPRINT`", + Name: "sign-by", + Usage: "Sign the image using a GPG key with the specified `FINGERPRINT`", + Destination: &opts.signByFingerprint, }, cli.StringFlag{ Name: "src-creds, screds", @@ -111,9 +118,10 @@ func copyCmd() cli.Command { Value: "", Usage: "`DIRECTORY` to use to store retrieved blobs (OCI layout destinations only)", }, - cli.StringFlag{ + cli.GenericFlag{ Name: "format, f", Usage: "`MANIFEST TYPE` (oci, v2s1, or v2s2) to use when saving image to directory using the 'dir:' transport (default is manifest type of source)", + Value: newOptionalStringValue(&opts.format), }, cli.BoolFlag{ Name: "dest-compress", @@ -153,8 +161,6 @@ func (opts *copyOptions) run(c *cli.Context) error { if err != nil { return fmt.Errorf("Invalid destination name %s: %v", c.Args()[1], err) } - signBy := c.String("sign-by") - removeSignatures := c.Bool("remove-signatures") sourceCtx, destinationCtx, err := contextsFromGlobalOptions(c) if err != nil { @@ -162,8 +168,8 @@ func (opts *copyOptions) run(c *cli.Context) error { } var manifestType string - if c.IsSet("format") { - switch c.String("format") { + if opts.format.present { + switch opts.format.value { case "oci": manifestType = imgspecv1.MediaTypeImageManifest case "v2s1": @@ -171,30 +177,28 @@ func (opts *copyOptions) run(c *cli.Context) error { case "v2s2": manifestType = manifest.DockerV2Schema2MediaType default: - return fmt.Errorf("unknown format %q. Choose one of the supported formats: 'oci', 'v2s1', or 'v2s2'", c.String("format")) + return fmt.Errorf("unknown format %q. Choose one of the supported formats: 'oci', 'v2s1', or 'v2s2'", opts.format.value) } } - if c.IsSet("additional-tag") { - for _, image := range c.StringSlice("additional-tag") { - ref, err := reference.ParseNormalizedNamed(image) - if err != nil { - return fmt.Errorf("error parsing additional-tag '%s': %v", image, err) - } - namedTagged, isNamedTagged := ref.(reference.NamedTagged) - if !isNamedTagged { - return fmt.Errorf("additional-tag '%s' must be a tagged reference", image) - } - destinationCtx.DockerArchiveAdditionalTags = append(destinationCtx.DockerArchiveAdditionalTags, namedTagged) + for _, image := range opts.additionalTags { + ref, err := reference.ParseNormalizedNamed(image) + if err != nil { + return fmt.Errorf("error parsing additional-tag '%s': %v", image, err) } + namedTagged, isNamedTagged := ref.(reference.NamedTagged) + if !isNamedTagged { + return fmt.Errorf("additional-tag '%s' must be a tagged reference", image) + } + destinationCtx.DockerArchiveAdditionalTags = append(destinationCtx.DockerArchiveAdditionalTags, namedTagged) } ctx, cancel := commandTimeoutContextFromGlobalOptions(c) defer cancel() _, err = copy.Image(ctx, policyContext, destRef, srcRef, ©.Options{ - RemoveSignatures: removeSignatures, - SignBy: signBy, + RemoveSignatures: opts.removeSignatures, + SignBy: opts.signByFingerprint, ReportWriter: os.Stdout, SourceCtx: sourceCtx, DestinationCtx: destinationCtx, diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index ab1b0781..fbad4f68 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -30,6 +30,7 @@ type inspectOutput struct { } type inspectOptions struct { + raw bool // Output the raw manifest instead of parsing information about the image } func inspectCmd() cli.Command { @@ -61,8 +62,9 @@ func inspectCmd() cli.Command { Usage: "require HTTPS and verify certificates when talking to container registries (defaults to true)", }, cli.BoolFlag{ - Name: "raw", - Usage: "output raw manifest", + Name: "raw", + Usage: "output raw manifest", + Destination: &opts.raw, }, cli.StringFlag{ Name: "creds", @@ -93,7 +95,7 @@ func (opts *inspectOptions) run(c *cli.Context) (retErr error) { if err != nil { return err } - if c.Bool("raw") { + if opts.raw { _, err := c.App.Writer.Write(rawManifest) if err != nil { return fmt.Errorf("Error writing manifest to standard output: %v", err) diff --git a/cmd/skopeo/signing.go b/cmd/skopeo/signing.go index fd0332e3..9d7a29a5 100644 --- a/cmd/skopeo/signing.go +++ b/cmd/skopeo/signing.go @@ -11,6 +11,7 @@ import ( ) type standaloneSignOptions struct { + output string // Output file path } func standaloneSignCmd() cli.Command { @@ -22,16 +23,16 @@ func standaloneSignCmd() cli.Command { Action: opts.run, Flags: []cli.Flag{ cli.StringFlag{ - Name: "output, o", - Usage: "output the signature to `SIGNATURE`", + Name: "output, o", + Usage: "output the signature to `SIGNATURE`", + Destination: &opts.output, }, }, } } func (opts *standaloneSignOptions) run(c *cli.Context) error { - outputFile := c.String("output") - if len(c.Args()) != 3 || outputFile == "" { + if len(c.Args()) != 3 || opts.output == "" { return errors.New("Usage: skopeo standalone-sign manifest docker-reference key-fingerprint -o signature") } manifestPath := c.Args()[0] @@ -53,8 +54,8 @@ func (opts *standaloneSignOptions) run(c *cli.Context) error { return fmt.Errorf("Error creating signature: %v", err) } - if err := ioutil.WriteFile(outputFile, signature, 0644); err != nil { - return fmt.Errorf("Error writing signature to %s: %v", outputFile, err) + if err := ioutil.WriteFile(opts.output, signature, 0644); err != nil { + return fmt.Errorf("Error writing signature to %s: %v", opts.output, err) } return nil } From 33b474b224936e25fa3f57aa238220cd3573b0a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Jul 2018 02:45:14 +0200 Subject: [PATCH 07/30] Create a globalOptions structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This works just like the command-specific options. Also moves the "Before:" handler into a separate method. Does not change behavior. Signed-off-by: Miloslav Trmač --- cmd/skopeo/main.go | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/cmd/skopeo/main.go b/cmd/skopeo/main.go index 6a2fd560..852e8cdf 100644 --- a/cmd/skopeo/main.go +++ b/cmd/skopeo/main.go @@ -15,8 +15,13 @@ import ( // and will be populated by the Makefile var gitCommit = "" +type globalOptions struct { +} + // createApp returns a cli.App to be run or tested. func createApp() *cli.App { + opts := globalOptions{} + app := cli.NewApp() app.EnableBashCompletion = true app.Name = "skopeo" @@ -65,15 +70,7 @@ func createApp() *cli.App { Usage: "timeout for the command execution", }, } - app.Before = func(c *cli.Context) error { - if c.GlobalBool("debug") { - logrus.SetLevel(logrus.DebugLevel) - } - if c.GlobalIsSet("tls-verify") { - logrus.Warn("'--tls-verify' is deprecated, please set this on the specific subcommand") - } - return nil - } + app.Before = opts.before app.Commands = []cli.Command{ copyCmd(), inspectCmd(), @@ -87,6 +84,17 @@ func createApp() *cli.App { return app } +// before is run by the cli package for any command, before running the command-specific handler. +func (opts *globalOptions) before(c *cli.Context) error { + if c.GlobalBool("debug") { + logrus.SetLevel(logrus.DebugLevel) + } + if c.GlobalIsSet("tls-verify") { + logrus.Warn("'--tls-verify' is deprecated, please set this on the specific subcommand") + } + return nil +} + func main() { if reexec.Init() { return From f30756a9bb27099a7240b9c9bb7f5be2c516cdae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Jul 2018 02:48:17 +0200 Subject: [PATCH 08/30] Use globalOptions for the debug flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This works just like the command-specific options. Handles only the single flag for now, others will be added as the infrastructure is built. Signed-off-by: Miloslav Trmač --- cmd/skopeo/main.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/skopeo/main.go b/cmd/skopeo/main.go index 852e8cdf..3760f5fa 100644 --- a/cmd/skopeo/main.go +++ b/cmd/skopeo/main.go @@ -16,6 +16,7 @@ import ( var gitCommit = "" type globalOptions struct { + debug bool // Enable debug output } // createApp returns a cli.App to be run or tested. @@ -33,8 +34,9 @@ func createApp() *cli.App { app.Usage = "Various operations with container images and container image registries" app.Flags = []cli.Flag{ cli.BoolFlag{ - Name: "debug", - Usage: "enable debug output", + Name: "debug", + Usage: "enable debug output", + Destination: &opts.debug, }, cli.BoolTFlag{ Name: "tls-verify", @@ -86,7 +88,7 @@ func createApp() *cli.App { // before is run by the cli package for any command, before running the command-specific handler. func (opts *globalOptions) before(c *cli.Context) error { - if c.GlobalBool("debug") { + if opts.debug { logrus.SetLevel(logrus.DebugLevel) } if c.GlobalIsSet("tls-verify") { From e1cc97d9d7be2a7bb2e645f49088050c11397feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Jul 2018 03:01:04 +0200 Subject: [PATCH 09/30] Use globalOptions for policy configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This requires us to propagate globalOptions to the per-command *Options state. Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 7 ++++--- cmd/skopeo/main.go | 30 ++++++++++++++++-------------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 23da3919..a9764c65 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -32,14 +32,15 @@ func contextsFromGlobalOptions(c *cli.Context) (*types.SystemContext, *types.Sys } type copyOptions struct { + global *globalOptions additionalTags cli.StringSlice // For docker-archive: destinations, in addition to the name:tag specified as destination, also add these removeSignatures bool // Do not copy signatures from the source image signByFingerprint string // Sign the image using a GPG key with the specified fingerprint format optionalString // Force conversion of the image to a specified format } -func copyCmd() cli.Command { - opts := copyOptions{} +func copyCmd(global *globalOptions) cli.Command { + opts := copyOptions{global: global} return cli.Command{ Name: "copy", Usage: "Copy an IMAGE-NAME from one location to another", @@ -147,7 +148,7 @@ func (opts *copyOptions) run(c *cli.Context) error { return errors.New("Exactly two arguments expected") } - policyContext, err := getPolicyContext(c) + policyContext, err := opts.global.getPolicyContext() if err != nil { return fmt.Errorf("Error loading trust policy: %v", err) } diff --git a/cmd/skopeo/main.go b/cmd/skopeo/main.go index 3760f5fa..c3b780c2 100644 --- a/cmd/skopeo/main.go +++ b/cmd/skopeo/main.go @@ -16,7 +16,9 @@ import ( var gitCommit = "" type globalOptions struct { - debug bool // Enable debug output + debug bool // Enable debug output + policyPath string // Path to a signature verification policy file + insecurePolicy bool // Use an "allow everything" signature verification policy } // createApp returns a cli.App to be run or tested. @@ -44,13 +46,14 @@ func createApp() *cli.App { Hidden: true, }, cli.StringFlag{ - Name: "policy", - Value: "", - Usage: "Path to a trust policy file", + Name: "policy", + Usage: "Path to a trust policy file", + Destination: &opts.policyPath, }, cli.BoolFlag{ - Name: "insecure-policy", - Usage: "run the tool without any policy check", + Name: "insecure-policy", + Usage: "run the tool without any policy check", + Destination: &opts.insecurePolicy, }, cli.StringFlag{ Name: "registries.d", @@ -74,7 +77,7 @@ func createApp() *cli.App { } app.Before = opts.before app.Commands = []cli.Command{ - copyCmd(), + copyCmd(&opts), inspectCmd(), layersCmd(), deleteCmd(), @@ -107,17 +110,16 @@ func main() { } } -// getPolicyContext handles the global "policy" flag. -func getPolicyContext(c *cli.Context) (*signature.PolicyContext, error) { - policyPath := c.GlobalString("policy") - var policy *signature.Policy // This could be cached across calls, if we had an application context. +// getPolicyContext returns a *signature.PolicyContext based on opts. +func (opts *globalOptions) getPolicyContext() (*signature.PolicyContext, error) { + var policy *signature.Policy // This could be cached across calls in opts. var err error - if c.GlobalBool("insecure-policy") { + if opts.insecurePolicy { policy = &signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}} - } else if policyPath == "" { + } else if opts.policyPath == "" { policy, err = signature.DefaultPolicy(nil) } else { - policy, err = signature.NewPolicyFromFile(policyPath) + policy, err = signature.NewPolicyFromFile(opts.policyPath) } if err != nil { return nil, err From ee8391db345501c62d2950677f75cfb3f6b04771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 3 Nov 2018 01:48:02 +0100 Subject: [PATCH 10/30] Use globalOptions for the global timeout option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace commandTimeoutContextFromGlobalOptions with globalOptions.commandTimeoutContext. This requires passing globalOptions to more per-command *Options state. Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 2 +- cmd/skopeo/delete.go | 7 ++++--- cmd/skopeo/inspect.go | 9 +++++---- cmd/skopeo/layers.go | 7 ++++--- cmd/skopeo/main.go | 31 +++++++++++++++++++++++-------- cmd/skopeo/utils.go | 9 --------- 6 files changed, 37 insertions(+), 28 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index a9764c65..90a533e5 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -194,7 +194,7 @@ func (opts *copyOptions) run(c *cli.Context) error { destinationCtx.DockerArchiveAdditionalTags = append(destinationCtx.DockerArchiveAdditionalTags, namedTagged) } - ctx, cancel := commandTimeoutContextFromGlobalOptions(c) + ctx, cancel := opts.global.commandTimeoutContext() defer cancel() _, err = copy.Image(ctx, policyContext, destRef, srcRef, ©.Options{ diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go index 4479f0a2..9eee301d 100644 --- a/cmd/skopeo/delete.go +++ b/cmd/skopeo/delete.go @@ -11,10 +11,11 @@ import ( ) type deleteOptions struct { + global *globalOptions } -func deleteCmd() cli.Command { - opts := deleteOptions{} +func deleteCmd(global *globalOptions) cli.Command { + opts := deleteOptions{global: global} return cli.Command{ Name: "delete", Usage: "Delete image IMAGE-NAME", @@ -66,7 +67,7 @@ func (opts *deleteOptions) run(c *cli.Context) error { return err } - ctx, cancel := commandTimeoutContextFromGlobalOptions(c) + ctx, cancel := opts.global.commandTimeoutContext() defer cancel() return ref.DeleteImage(ctx, sys) } diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index fbad4f68..9c8ecdd8 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -30,11 +30,12 @@ type inspectOutput struct { } type inspectOptions struct { - raw bool // Output the raw manifest instead of parsing information about the image + global *globalOptions + raw bool // Output the raw manifest instead of parsing information about the image } -func inspectCmd() cli.Command { - opts := inspectOptions{} +func inspectCmd(global *globalOptions) cli.Command { + opts := inspectOptions{global: global} return cli.Command{ Name: "inspect", Usage: "Inspect image IMAGE-NAME", @@ -77,7 +78,7 @@ func inspectCmd() cli.Command { } func (opts *inspectOptions) run(c *cli.Context) (retErr error) { - ctx, cancel := commandTimeoutContextFromGlobalOptions(c) + ctx, cancel := opts.global.commandTimeoutContext() defer cancel() img, err := parseImage(ctx, c) diff --git a/cmd/skopeo/layers.go b/cmd/skopeo/layers.go index a5b76ae6..1cb77f53 100644 --- a/cmd/skopeo/layers.go +++ b/cmd/skopeo/layers.go @@ -16,10 +16,11 @@ import ( ) type layersOptions struct { + global *globalOptions } -func layersCmd() cli.Command { - opts := &layersOptions{} +func layersCmd(global *globalOptions) cli.Command { + opts := &layersOptions{global: global} return cli.Command{ Name: "layers", Usage: "Get layers of IMAGE-NAME", @@ -35,7 +36,7 @@ func (opts *layersOptions) run(c *cli.Context) (retErr error) { return errors.New("Usage: layers imageReference [layer...]") } - ctx, cancel := commandTimeoutContextFromGlobalOptions(c) + ctx, cancel := opts.global.commandTimeoutContext() defer cancel() sys, err := contextFromGlobalOptions(c, "") diff --git a/cmd/skopeo/main.go b/cmd/skopeo/main.go index c3b780c2..dc1bea48 100644 --- a/cmd/skopeo/main.go +++ b/cmd/skopeo/main.go @@ -1,8 +1,10 @@ package main import ( + "context" "fmt" "os" + "time" "github.com/containers/image/signature" "github.com/containers/skopeo/version" @@ -16,9 +18,10 @@ import ( var gitCommit = "" type globalOptions struct { - debug bool // Enable debug output - policyPath string // Path to a signature verification policy file - insecurePolicy bool // Use an "allow everything" signature verification policy + debug bool // Enable debug output + policyPath string // Path to a signature verification policy file + insecurePolicy bool // Use an "allow everything" signature verification policy + commandTimeout time.Duration // Timeout for the command execution } // createApp returns a cli.App to be run or tested. @@ -71,16 +74,17 @@ func createApp() *cli.App { Usage: "use `OS` instead of the running OS for choosing images", }, cli.DurationFlag{ - Name: "command-timeout", - Usage: "timeout for the command execution", + Name: "command-timeout", + Usage: "timeout for the command execution", + Destination: &opts.commandTimeout, }, } app.Before = opts.before app.Commands = []cli.Command{ copyCmd(&opts), - inspectCmd(), - layersCmd(), - deleteCmd(), + inspectCmd(&opts), + layersCmd(&opts), + deleteCmd(&opts), manifestDigestCmd(), standaloneSignCmd(), standaloneVerifyCmd(), @@ -126,3 +130,14 @@ func (opts *globalOptions) getPolicyContext() (*signature.PolicyContext, error) } return signature.NewPolicyContext(policy) } + +// commandTimeoutContext returns a context.Context and a cancellation callback based on opts. +// The caller should usually "defer cancel()" immediately after calling this. +func (opts *globalOptions) commandTimeoutContext() (context.Context, context.CancelFunc) { + ctx := context.Background() + var cancel context.CancelFunc = func() {} + if opts.commandTimeout > 0 { + ctx, cancel = context.WithTimeout(ctx, opts.commandTimeout) + } + return ctx, cancel +} diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index 1fb29eb1..f94e01dc 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -42,15 +42,6 @@ func contextFromGlobalOptions(c *cli.Context, flagPrefix string) (*types.SystemC return ctx, nil } -func commandTimeoutContextFromGlobalOptions(c *cli.Context) (context.Context, context.CancelFunc) { - ctx := context.Background() - var cancel context.CancelFunc = func() {} - if c.GlobalDuration("command-timeout") > 0 { - ctx, cancel = context.WithTimeout(ctx, c.GlobalDuration("command-timeout")) - } - return ctx, cancel -} - func parseCreds(creds string) (string, string, error) { if creds == "" { return "", "", errors.New("credentials can't be empty") From 3ea3965e5eb179eec92bb9d533fdb53ab1766bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Jul 2018 03:18:59 +0200 Subject: [PATCH 11/30] Use globalOptions for setting up types.SystemContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit contextFromGlobalOptions now uses globalOptions instead of cli.Context.Global* . That required passing globalOptions through a few more functions. Now, "all" that is left are all the non-global options handled in contextFromGlobalOptions. Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 8 +++---- cmd/skopeo/delete.go | 2 +- cmd/skopeo/inspect.go | 4 ++-- cmd/skopeo/layers.go | 4 ++-- cmd/skopeo/main.go | 45 ++++++++++++++++++++++------------------ cmd/skopeo/main_test.go | 2 +- cmd/skopeo/utils.go | 23 ++++++++++---------- cmd/skopeo/utils_test.go | 26 +++++++++++------------ 8 files changed, 59 insertions(+), 55 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 90a533e5..81f6f35d 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -17,13 +17,13 @@ import ( ) // contextsFromGlobalOptions returns source and destionation types.SystemContext depending on c. -func contextsFromGlobalOptions(c *cli.Context) (*types.SystemContext, *types.SystemContext, error) { - sourceCtx, err := contextFromGlobalOptions(c, "src-") +func contextsFromGlobalOptions(c *cli.Context, global *globalOptions) (*types.SystemContext, *types.SystemContext, error) { + sourceCtx, err := contextFromGlobalOptions(c, global, "src-") if err != nil { return nil, nil, err } - destinationCtx, err := contextFromGlobalOptions(c, "dest-") + destinationCtx, err := contextFromGlobalOptions(c, global, "dest-") if err != nil { return nil, nil, err } @@ -163,7 +163,7 @@ func (opts *copyOptions) run(c *cli.Context) error { return fmt.Errorf("Invalid destination name %s: %v", c.Args()[1], err) } - sourceCtx, destinationCtx, err := contextsFromGlobalOptions(c) + sourceCtx, destinationCtx, err := contextsFromGlobalOptions(c, opts.global) if err != nil { return err } diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go index 9eee301d..e6e173f9 100644 --- a/cmd/skopeo/delete.go +++ b/cmd/skopeo/delete.go @@ -62,7 +62,7 @@ func (opts *deleteOptions) run(c *cli.Context) error { return fmt.Errorf("Invalid source name %s: %v", c.Args()[0], err) } - sys, err := contextFromGlobalOptions(c, "") + sys, err := contextFromGlobalOptions(c, opts.global, "") if err != nil { return err } diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 9c8ecdd8..15a38ccf 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -81,7 +81,7 @@ func (opts *inspectOptions) run(c *cli.Context) (retErr error) { ctx, cancel := opts.global.commandTimeoutContext() defer cancel() - img, err := parseImage(ctx, c) + img, err := parseImage(ctx, c, opts.global) if err != nil { return err } @@ -127,7 +127,7 @@ func (opts *inspectOptions) run(c *cli.Context) (retErr error) { outputData.Name = dockerRef.Name() } if img.Reference().Transport() == docker.Transport { - sys, err := contextFromGlobalOptions(c, "") + sys, err := contextFromGlobalOptions(c, opts.global, "") if err != nil { return err } diff --git a/cmd/skopeo/layers.go b/cmd/skopeo/layers.go index 1cb77f53..f503af6e 100644 --- a/cmd/skopeo/layers.go +++ b/cmd/skopeo/layers.go @@ -39,12 +39,12 @@ func (opts *layersOptions) run(c *cli.Context) (retErr error) { ctx, cancel := opts.global.commandTimeoutContext() defer cancel() - sys, err := contextFromGlobalOptions(c, "") + sys, err := contextFromGlobalOptions(c, opts.global, "") if err != nil { return err } cache := blobinfocache.DefaultCache(sys) - rawSource, err := parseImageSource(ctx, c, c.Args()[0]) + rawSource, err := parseImageSource(ctx, c, opts.global, c.Args()[0]) if err != nil { return err } diff --git a/cmd/skopeo/main.go b/cmd/skopeo/main.go index dc1bea48..fb56568c 100644 --- a/cmd/skopeo/main.go +++ b/cmd/skopeo/main.go @@ -18,14 +18,18 @@ import ( var gitCommit = "" type globalOptions struct { - debug bool // Enable debug output - policyPath string // Path to a signature verification policy file - insecurePolicy bool // Use an "allow everything" signature verification policy - commandTimeout time.Duration // Timeout for the command execution + debug bool // Enable debug output + tlsVerify optionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:) + policyPath string // Path to a signature verification policy file + insecurePolicy bool // Use an "allow everything" signature verification policy + registriesDirPath string // Path to a "registries.d" registry configuratio directory + overrideArch string // Architecture to use for choosing images, instead of the runtime one + overrideOS string // OS to use for choosing images, instead of the runtime one + commandTimeout time.Duration // Timeout for the command execution } -// createApp returns a cli.App to be run or tested. -func createApp() *cli.App { +// createApp returns a cli.App, and the underlying globalOptions object, to be run or tested. +func createApp() (*cli.App, *globalOptions) { opts := globalOptions{} app := cli.NewApp() @@ -43,10 +47,11 @@ func createApp() *cli.App { Usage: "enable debug output", Destination: &opts.debug, }, - cli.BoolTFlag{ + cli.GenericFlag{ Name: "tls-verify", Usage: "require HTTPS and verify certificates when talking to container registries (defaults to true)", Hidden: true, + Value: newOptionalBoolValue(&opts.tlsVerify), }, cli.StringFlag{ Name: "policy", @@ -59,19 +64,19 @@ func createApp() *cli.App { Destination: &opts.insecurePolicy, }, cli.StringFlag{ - Name: "registries.d", - Value: "", - Usage: "use registry configuration files in `DIR` (e.g. for container signature storage)", + Name: "registries.d", + Usage: "use registry configuration files in `DIR` (e.g. for container signature storage)", + Destination: &opts.registriesDirPath, }, cli.StringFlag{ - Name: "override-arch", - Value: "", - Usage: "use `ARCH` instead of the architecture of the machine for choosing images", + Name: "override-arch", + Usage: "use `ARCH` instead of the architecture of the machine for choosing images", + Destination: &opts.overrideArch, }, cli.StringFlag{ - Name: "override-os", - Value: "", - Usage: "use `OS` instead of the running OS for choosing images", + Name: "override-os", + Usage: "use `OS` instead of the running OS for choosing images", + Destination: &opts.overrideOS, }, cli.DurationFlag{ Name: "command-timeout", @@ -90,15 +95,15 @@ func createApp() *cli.App { standaloneVerifyCmd(), untrustedSignatureDumpCmd(), } - return app + return app, &opts } // before is run by the cli package for any command, before running the command-specific handler. -func (opts *globalOptions) before(c *cli.Context) error { +func (opts *globalOptions) before(_ *cli.Context) error { if opts.debug { logrus.SetLevel(logrus.DebugLevel) } - if c.GlobalIsSet("tls-verify") { + if opts.tlsVerify.present { logrus.Warn("'--tls-verify' is deprecated, please set this on the specific subcommand") } return nil @@ -108,7 +113,7 @@ func main() { if reexec.Init() { return } - app := createApp() + app, _ := createApp() if err := app.Run(os.Args); err != nil { logrus.Fatal(err) } diff --git a/cmd/skopeo/main_test.go b/cmd/skopeo/main_test.go index 42df5069..8b1c421f 100644 --- a/cmd/skopeo/main_test.go +++ b/cmd/skopeo/main_test.go @@ -5,7 +5,7 @@ import "bytes" // runSkopeo creates an app object and runs it with args, with an implied first "skopeo". // Returns output intended for stdout and the returned error, if any. func runSkopeo(args ...string) (string, error) { - app := createApp() + app, _ := createApp() stdout := bytes.Buffer{} app.Writer = &stdout args = append([]string{"skopeo"}, args...) diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index f94e01dc..d6fa5375 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -10,11 +10,11 @@ import ( "github.com/urfave/cli" ) -func contextFromGlobalOptions(c *cli.Context, flagPrefix string) (*types.SystemContext, error) { +func contextFromGlobalOptions(c *cli.Context, global *globalOptions, flagPrefix string) (*types.SystemContext, error) { ctx := &types.SystemContext{ - RegistriesDirPath: c.GlobalString("registries.d"), - ArchitectureChoice: c.GlobalString("override-arch"), - OSChoice: c.GlobalString("override-os"), + RegistriesDirPath: global.registriesDirPath, + ArchitectureChoice: global.overrideArch, + OSChoice: global.overrideOS, DockerCertPath: c.String(flagPrefix + "cert-dir"), OSTreeTmpDirPath: c.String(flagPrefix + "ostree-tmp-dir"), OCISharedBlobDirPath: c.String(flagPrefix + "shared-blob-dir"), @@ -24,10 +24,9 @@ func contextFromGlobalOptions(c *cli.Context, flagPrefix string) (*types.SystemC DockerDaemonCertPath: c.String(flagPrefix + "cert-dir"), DockerDaemonInsecureSkipTLSVerify: !c.BoolT(flagPrefix + "tls-verify"), } - // DEPRECATED: we support --tls-verify for backward compatibility, but override - // it if per-subcommand flags are provided (see below). - if c.GlobalIsSet("tls-verify") { - ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!c.GlobalBoolT("tls-verify")) + // DEPRECATED: We support this for backward compatibility, but override it if a per-image flag is provided. + if global.tlsVerify.present { + ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!global.tlsVerify.value) } if c.IsSet(flagPrefix + "tls-verify") { ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!c.BoolT(flagPrefix + "tls-verify")) @@ -69,13 +68,13 @@ func getDockerAuth(creds string) (*types.DockerAuthConfig, error) { // parseImage converts image URL-like string to an initialized handler for that image. // The caller must call .Close() on the returned ImageCloser. -func parseImage(ctx context.Context, c *cli.Context) (types.ImageCloser, error) { +func parseImage(ctx context.Context, c *cli.Context, global *globalOptions) (types.ImageCloser, error) { imgName := c.Args().First() ref, err := alltransports.ParseImageName(imgName) if err != nil { return nil, err } - sys, err := contextFromGlobalOptions(c, "") + sys, err := contextFromGlobalOptions(c, global, "") if err != nil { return nil, err } @@ -84,12 +83,12 @@ func parseImage(ctx context.Context, c *cli.Context) (types.ImageCloser, error) // parseImageSource converts image URL-like string to an ImageSource. // The caller must call .Close() on the returned ImageSource. -func parseImageSource(ctx context.Context, c *cli.Context, name string) (types.ImageSource, error) { +func parseImageSource(ctx context.Context, c *cli.Context, global *globalOptions, name string) (types.ImageSource, error) { ref, err := alltransports.ParseImageName(name) if err != nil { return nil, err } - sys, err := contextFromGlobalOptions(c, "") + sys, err := contextFromGlobalOptions(c, global, "") if err != nil { return nil, err } diff --git a/cmd/skopeo/utils_test.go b/cmd/skopeo/utils_test.go index b174daf6..04b49913 100644 --- a/cmd/skopeo/utils_test.go +++ b/cmd/skopeo/utils_test.go @@ -12,8 +12,8 @@ import ( // fakeContext creates inputs for contextFromGlobalOptions. // NOTE: This is QUITE FAKE; none of the urfave/cli normalization and the like happens. -func fakeContext(t *testing.T, cmdName string, globalFlags []string, cmdFlags []string) *cli.Context { - app := createApp() +func fakeContext(t *testing.T, cmdName string, globalFlags []string, cmdFlags []string) (*cli.Context, *globalOptions) { + app, global := createApp() globalSet := flag.NewFlagSet(app.Name, flag.ContinueOnError) for _, f := range app.Flags { @@ -31,7 +31,7 @@ func fakeContext(t *testing.T, cmdName string, globalFlags []string, cmdFlags [] } err = cmdSet.Parse(cmdFlags) require.NoError(t, err) - return cli.NewContext(app, cmdSet, globalCtx) + return cli.NewContext(app, cmdSet, globalCtx), global } func TestContextFromGlobalOptions(t *testing.T) { @@ -39,21 +39,21 @@ func TestContextFromGlobalOptions(t *testing.T) { // FIXME FIXME: Apparently BoolT values are set to false if the flag is not declared for the specific subcommand!! // Default state - c := fakeContext(t, "copy", []string{}, []string{}) - res, err := contextFromGlobalOptions(c, "dest-") + c, global := fakeContext(t, "copy", []string{}, []string{}) + res, err := contextFromGlobalOptions(c, global, "dest-") require.NoError(t, err) assert.Equal(t, &types.SystemContext{}, res) // Explicitly set everything to default, except for when the default is “not present” - c = fakeContext(t, "copy", []string{}, []string{ + c, global = fakeContext(t, "copy", []string{}, []string{ "--dest-compress=false", }) - res, err = contextFromGlobalOptions(c, "dest-") + res, err = contextFromGlobalOptions(c, global, "dest-") require.NoError(t, err) assert.Equal(t, &types.SystemContext{}, res) // Set everything to non-default values. - c = fakeContext(t, "copy", []string{ + c, global = fakeContext(t, "copy", []string{ "--registries.d", "/srv/registries.d", "--override-arch", "overridden-arch", "--override-os", "overridden-os", @@ -67,7 +67,7 @@ func TestContextFromGlobalOptions(t *testing.T) { "--dest-tls-verify=false", "--dest-creds", "creds-user:creds-password", }) - res, err = contextFromGlobalOptions(c, "dest-") + res, err = contextFromGlobalOptions(c, global, "dest-") require.NoError(t, err) assert.Equal(t, &types.SystemContext{ RegistriesDirPath: "/srv/registries.d", @@ -109,15 +109,15 @@ func TestContextFromGlobalOptions(t *testing.T) { if c.cmd != "" { cmdFlags = append(cmdFlags, "--dest-tls-verify="+c.cmd) } - ctx := fakeContext(t, "copy", globalFlags, cmdFlags) - res, err = contextFromGlobalOptions(ctx, "dest-") + ctx, global := fakeContext(t, "copy", globalFlags, cmdFlags) + res, err = contextFromGlobalOptions(ctx, global, "dest-") require.NoError(t, err) assert.Equal(t, c.expectedDocker, res.DockerInsecureSkipTLSVerify, "%#v", c) assert.Equal(t, c.expectedDockerDaemon, res.DockerDaemonInsecureSkipTLSVerify, "%#v", c) } // Invalid option values - c = fakeContext(t, "copy", []string{}, []string{"--dest-creds", ""}) - _, err = contextFromGlobalOptions(c, "dest-") + c, global = fakeContext(t, "copy", []string{}, []string{"--dest-creds", ""}) + _, err = contextFromGlobalOptions(c, global, "dest-") assert.Error(t, err) } From c769c7789eb9aeb3575a1413876e584d9089f279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Mon, 16 Jul 2018 22:57:02 +0200 Subject: [PATCH 12/30] Introduce imageOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is similar to the previous *Options structures, but this one will support differing sets of options, in particular for the copy source/destination. The way the return values of imageFlags() are integrated into creation of a cli.Command forces fakeContext() in tests to do very ugly filtering to have a working *imageOptions available without having a copyCmd() cooperate to give it to us. Rather than extend copyCmd(), we do the filtering, because the reliance on copyCmd() will go away after all flags are migrated, and so will the filtering and fakeContext() complexity. Finally, rename contextFromGlobalOptions to not lie about only caring about global options. This only introduces the infrastructure, all flags continue to be handled in the old way. Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 24 +++++++++++------ cmd/skopeo/delete.go | 13 +++++++--- cmd/skopeo/inspect.go | 15 +++++++---- cmd/skopeo/layers.go | 12 ++++++--- cmd/skopeo/utils.go | 32 ++++++++++++++++------- cmd/skopeo/utils_test.go | 56 +++++++++++++++++++++++++++------------- 6 files changed, 104 insertions(+), 48 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 81f6f35d..fdff7c1b 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -16,14 +16,14 @@ import ( "github.com/urfave/cli" ) -// contextsFromGlobalOptions returns source and destionation types.SystemContext depending on c. -func contextsFromGlobalOptions(c *cli.Context, global *globalOptions) (*types.SystemContext, *types.SystemContext, error) { - sourceCtx, err := contextFromGlobalOptions(c, global, "src-") +// contextsFromCopyOptions returns source and destionation types.SystemContext depending on c. +func contextsFromCopyOptions(c *cli.Context, opts *copyOptions) (*types.SystemContext, *types.SystemContext, error) { + sourceCtx, err := contextFromImageOptions(c, opts.srcImage, "src-") if err != nil { return nil, nil, err } - destinationCtx, err := contextFromGlobalOptions(c, global, "dest-") + destinationCtx, err := contextFromImageOptions(c, opts.destImage, "dest-") if err != nil { return nil, nil, err } @@ -33,6 +33,8 @@ func contextsFromGlobalOptions(c *cli.Context, global *globalOptions) (*types.Sy type copyOptions struct { global *globalOptions + srcImage *imageOptions + destImage *imageOptions additionalTags cli.StringSlice // For docker-archive: destinations, in addition to the name:tag specified as destination, also add these removeSignatures bool // Do not copy signatures from the source image signByFingerprint string // Sign the image using a GPG key with the specified fingerprint @@ -40,7 +42,13 @@ type copyOptions struct { } func copyCmd(global *globalOptions) cli.Command { - opts := copyOptions{global: global} + srcFlags, srcOpts := imageFlags(global, "src-") + destFlags, destOpts := imageFlags(global, "dest-") + opts := copyOptions{global: global, + srcImage: srcOpts, + destImage: destOpts, + } + return cli.Command{ Name: "copy", Usage: "Copy an IMAGE-NAME from one location to another", @@ -56,7 +64,7 @@ func copyCmd(global *globalOptions) cli.Command { ArgsUsage: "SOURCE-IMAGE DESTINATION-IMAGE", Action: opts.run, // FIXME: Do we need to namespace the GPG aspect? - Flags: []cli.Flag{ + Flags: append(append([]cli.Flag{ cli.StringSliceFlag{ Name: "additional-tag", Usage: "additional tags (supports docker-archive)", @@ -138,7 +146,7 @@ func copyCmd(global *globalOptions) cli.Command { Value: "", Usage: "use docker daemon host at `HOST` (docker-daemon destinations only)", }, - }, + }, srcFlags...), destFlags...), } } @@ -163,7 +171,7 @@ func (opts *copyOptions) run(c *cli.Context) error { return fmt.Errorf("Invalid destination name %s: %v", c.Args()[1], err) } - sourceCtx, destinationCtx, err := contextsFromGlobalOptions(c, opts.global) + sourceCtx, destinationCtx, err := contextsFromCopyOptions(c, opts) if err != nil { return err } diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go index e6e173f9..5a9de127 100644 --- a/cmd/skopeo/delete.go +++ b/cmd/skopeo/delete.go @@ -12,10 +12,15 @@ import ( type deleteOptions struct { global *globalOptions + image *imageOptions } func deleteCmd(global *globalOptions) cli.Command { - opts := deleteOptions{global: global} + imageFlags, imageOpts := imageFlags(global, "") + opts := deleteOptions{ + global: global, + image: imageOpts, + } return cli.Command{ Name: "delete", Usage: "Delete image IMAGE-NAME", @@ -29,7 +34,7 @@ func deleteCmd(global *globalOptions) cli.Command { `, strings.Join(transports.ListNames(), ", ")), ArgsUsage: "IMAGE-NAME", Action: opts.run, - Flags: []cli.Flag{ + Flags: append([]cli.Flag{ cli.StringFlag{ Name: "authfile", Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", @@ -48,7 +53,7 @@ func deleteCmd(global *globalOptions) cli.Command { Name: "tls-verify", Usage: "require HTTPS and verify certificates when talking to container registries (defaults to true)", }, - }, + }, imageFlags...), } } @@ -62,7 +67,7 @@ func (opts *deleteOptions) run(c *cli.Context) error { return fmt.Errorf("Invalid source name %s: %v", c.Args()[0], err) } - sys, err := contextFromGlobalOptions(c, opts.global, "") + sys, err := contextFromImageOptions(c, opts.image, "") if err != nil { return err } diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 15a38ccf..d35b4a75 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -31,11 +31,16 @@ type inspectOutput struct { type inspectOptions struct { global *globalOptions + image *imageOptions raw bool // Output the raw manifest instead of parsing information about the image } func inspectCmd(global *globalOptions) cli.Command { - opts := inspectOptions{global: global} + imageFlags, imageOpts := imageFlags(global, "") + opts := inspectOptions{ + global: global, + image: imageOpts, + } return cli.Command{ Name: "inspect", Usage: "Inspect image IMAGE-NAME", @@ -48,7 +53,7 @@ func inspectCmd(global *globalOptions) cli.Command { See skopeo(1) section "IMAGE NAMES" for the expected format `, strings.Join(transports.ListNames(), ", ")), ArgsUsage: "IMAGE-NAME", - Flags: []cli.Flag{ + Flags: append([]cli.Flag{ cli.StringFlag{ Name: "authfile", Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", @@ -72,7 +77,7 @@ func inspectCmd(global *globalOptions) cli.Command { Value: "", Usage: "Use `USERNAME[:PASSWORD]` for accessing the registry", }, - }, + }, imageFlags...), Action: opts.run, } } @@ -81,7 +86,7 @@ func (opts *inspectOptions) run(c *cli.Context) (retErr error) { ctx, cancel := opts.global.commandTimeoutContext() defer cancel() - img, err := parseImage(ctx, c, opts.global) + img, err := parseImage(ctx, c, opts.image) if err != nil { return err } @@ -127,7 +132,7 @@ func (opts *inspectOptions) run(c *cli.Context) (retErr error) { outputData.Name = dockerRef.Name() } if img.Reference().Transport() == docker.Transport { - sys, err := contextFromGlobalOptions(c, opts.global, "") + sys, err := contextFromImageOptions(c, opts.image, "") if err != nil { return err } diff --git a/cmd/skopeo/layers.go b/cmd/skopeo/layers.go index f503af6e..da04b625 100644 --- a/cmd/skopeo/layers.go +++ b/cmd/skopeo/layers.go @@ -17,16 +17,22 @@ import ( type layersOptions struct { global *globalOptions + image *imageOptions } func layersCmd(global *globalOptions) cli.Command { - opts := &layersOptions{global: global} + imageFlags, imageOpts := imageFlags(global, "") + opts := layersOptions{ + global: global, + image: imageOpts, + } return cli.Command{ Name: "layers", Usage: "Get layers of IMAGE-NAME", ArgsUsage: "IMAGE-NAME [LAYER...]", Hidden: true, Action: opts.run, + Flags: imageFlags, } } @@ -39,12 +45,12 @@ func (opts *layersOptions) run(c *cli.Context) (retErr error) { ctx, cancel := opts.global.commandTimeoutContext() defer cancel() - sys, err := contextFromGlobalOptions(c, opts.global, "") + sys, err := contextFromImageOptions(c, opts.image, "") if err != nil { return err } cache := blobinfocache.DefaultCache(sys) - rawSource, err := parseImageSource(ctx, c, opts.global, c.Args()[0]) + rawSource, err := parseImageSource(ctx, c, opts.image, c.Args()[0]) if err != nil { return err } diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index d6fa5375..79b0a182 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -10,11 +10,23 @@ import ( "github.com/urfave/cli" ) -func contextFromGlobalOptions(c *cli.Context, global *globalOptions, flagPrefix string) (*types.SystemContext, error) { +// imageOptions collects CLI flags which are the same across subcommands, but may be different for each image +// (e.g. may differ between the source and destination of a copy) +type imageOptions struct { + global *globalOptions // May be shared across several imageOptions instances. +} + +// imageFlags prepares a collection of CLI flags writing into imageOptions, and the managed imageOptions structure. +func imageFlags(global *globalOptions, flagPrefix string) ([]cli.Flag, *imageOptions) { + opts := imageOptions{global: global} + return []cli.Flag{}, &opts +} + +func contextFromImageOptions(c *cli.Context, opts *imageOptions, flagPrefix string) (*types.SystemContext, error) { ctx := &types.SystemContext{ - RegistriesDirPath: global.registriesDirPath, - ArchitectureChoice: global.overrideArch, - OSChoice: global.overrideOS, + RegistriesDirPath: opts.global.registriesDirPath, + ArchitectureChoice: opts.global.overrideArch, + OSChoice: opts.global.overrideOS, DockerCertPath: c.String(flagPrefix + "cert-dir"), OSTreeTmpDirPath: c.String(flagPrefix + "ostree-tmp-dir"), OCISharedBlobDirPath: c.String(flagPrefix + "shared-blob-dir"), @@ -25,8 +37,8 @@ func contextFromGlobalOptions(c *cli.Context, global *globalOptions, flagPrefix DockerDaemonInsecureSkipTLSVerify: !c.BoolT(flagPrefix + "tls-verify"), } // DEPRECATED: We support this for backward compatibility, but override it if a per-image flag is provided. - if global.tlsVerify.present { - ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!global.tlsVerify.value) + if opts.global.tlsVerify.present { + ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!opts.global.tlsVerify.value) } if c.IsSet(flagPrefix + "tls-verify") { ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!c.BoolT(flagPrefix + "tls-verify")) @@ -68,13 +80,13 @@ func getDockerAuth(creds string) (*types.DockerAuthConfig, error) { // parseImage converts image URL-like string to an initialized handler for that image. // The caller must call .Close() on the returned ImageCloser. -func parseImage(ctx context.Context, c *cli.Context, global *globalOptions) (types.ImageCloser, error) { +func parseImage(ctx context.Context, c *cli.Context, opts *imageOptions) (types.ImageCloser, error) { imgName := c.Args().First() ref, err := alltransports.ParseImageName(imgName) if err != nil { return nil, err } - sys, err := contextFromGlobalOptions(c, global, "") + sys, err := contextFromImageOptions(c, opts, "") if err != nil { return nil, err } @@ -83,12 +95,12 @@ func parseImage(ctx context.Context, c *cli.Context, global *globalOptions) (typ // parseImageSource converts image URL-like string to an ImageSource. // The caller must call .Close() on the returned ImageSource. -func parseImageSource(ctx context.Context, c *cli.Context, global *globalOptions, name string) (types.ImageSource, error) { +func parseImageSource(ctx context.Context, c *cli.Context, opts *imageOptions, name string) (types.ImageSource, error) { ref, err := alltransports.ParseImageName(name) if err != nil { return nil, err } - sys, err := contextFromGlobalOptions(c, global, "") + sys, err := contextFromImageOptions(c, opts, "") if err != nil { return nil, err } diff --git a/cmd/skopeo/utils_test.go b/cmd/skopeo/utils_test.go index 04b49913..44e61d93 100644 --- a/cmd/skopeo/utils_test.go +++ b/cmd/skopeo/utils_test.go @@ -2,6 +2,7 @@ package main import ( "flag" + "strings" "testing" "github.com/containers/image/types" @@ -10,10 +11,10 @@ import ( "github.com/urfave/cli" ) -// fakeContext creates inputs for contextFromGlobalOptions. +// fakeContext creates inputs for contextFromImageOptions. // NOTE: This is QUITE FAKE; none of the urfave/cli normalization and the like happens. -func fakeContext(t *testing.T, cmdName string, globalFlags []string, cmdFlags []string) (*cli.Context, *globalOptions) { - app, global := createApp() +func fakeContext(t *testing.T, cmdName string, flagPrefix string, globalFlags []string, cmdFlags []string) (*cli.Context, *imageOptions) { + app, globalOpts := createApp() globalSet := flag.NewFlagSet(app.Name, flag.ContinueOnError) for _, f := range app.Flags { @@ -25,35 +26,54 @@ func fakeContext(t *testing.T, cmdName string, globalFlags []string, cmdFlags [] cmd := app.Command(cmdName) require.NotNil(t, cmd) - cmdSet := flag.NewFlagSet(cmd.Name, flag.ContinueOnError) - for _, f := range cmd.Flags { - f.Apply(cmdSet) + + imageFlags, imageOpts := imageFlags(globalOpts, flagPrefix) + appliedFlags := map[string]struct{}{} + // Ugly: cmd.Flags includes imageFlags as well. For now, we need cmd.Flags to apply here + // to be able to test the non-Destination: flags, but we must not apply the same flag name twice. + // So, primarily use imageFlags (so that Destination: is used as expected), and then follow up with + // the remaining flags from cmd.Flags (so that cli.Context.String() etc. works). + // This is horribly ugly, but all of this will disappear within this PR. + firstName := func(f cli.Flag) string { // We even need to recognize "dest-creds,dcreds". This will disappear as well. + return strings.Split(f.GetName(), ",")[0] } + cmdSet := flag.NewFlagSet(cmd.Name, flag.ContinueOnError) + for _, f := range imageFlags { + f.Apply(cmdSet) + appliedFlags[firstName(f)] = struct{}{} + } + for _, f := range cmd.Flags { + if _, ok := appliedFlags[firstName(f)]; !ok { + f.Apply(cmdSet) + appliedFlags[firstName(f)] = struct{}{} + } + } + err = cmdSet.Parse(cmdFlags) require.NoError(t, err) - return cli.NewContext(app, cmdSet, globalCtx), global + return cli.NewContext(app, cmdSet, globalCtx), imageOpts } -func TestContextFromGlobalOptions(t *testing.T) { +func TestContextFromImageOptions(t *testing.T) { // FIXME: All of this only tests (skopeo copy --dest) // FIXME FIXME: Apparently BoolT values are set to false if the flag is not declared for the specific subcommand!! // Default state - c, global := fakeContext(t, "copy", []string{}, []string{}) - res, err := contextFromGlobalOptions(c, global, "dest-") + c, opts := fakeContext(t, "copy", "dest-", []string{}, []string{}) + res, err := contextFromImageOptions(c, opts, "dest-") require.NoError(t, err) assert.Equal(t, &types.SystemContext{}, res) // Explicitly set everything to default, except for when the default is “not present” - c, global = fakeContext(t, "copy", []string{}, []string{ + c, opts = fakeContext(t, "copy", "dest-", []string{}, []string{ "--dest-compress=false", }) - res, err = contextFromGlobalOptions(c, global, "dest-") + res, err = contextFromImageOptions(c, opts, "dest-") require.NoError(t, err) assert.Equal(t, &types.SystemContext{}, res) // Set everything to non-default values. - c, global = fakeContext(t, "copy", []string{ + c, opts = fakeContext(t, "copy", "dest-", []string{ "--registries.d", "/srv/registries.d", "--override-arch", "overridden-arch", "--override-os", "overridden-os", @@ -67,7 +87,7 @@ func TestContextFromGlobalOptions(t *testing.T) { "--dest-tls-verify=false", "--dest-creds", "creds-user:creds-password", }) - res, err = contextFromGlobalOptions(c, global, "dest-") + res, err = contextFromImageOptions(c, opts, "dest-") require.NoError(t, err) assert.Equal(t, &types.SystemContext{ RegistriesDirPath: "/srv/registries.d", @@ -109,15 +129,15 @@ func TestContextFromGlobalOptions(t *testing.T) { if c.cmd != "" { cmdFlags = append(cmdFlags, "--dest-tls-verify="+c.cmd) } - ctx, global := fakeContext(t, "copy", globalFlags, cmdFlags) - res, err = contextFromGlobalOptions(ctx, global, "dest-") + ctx, opts := fakeContext(t, "copy", "dest-", globalFlags, cmdFlags) + res, err = contextFromImageOptions(ctx, opts, "dest-") require.NoError(t, err) assert.Equal(t, c.expectedDocker, res.DockerInsecureSkipTLSVerify, "%#v", c) assert.Equal(t, c.expectedDockerDaemon, res.DockerDaemonInsecureSkipTLSVerify, "%#v", c) } // Invalid option values - c, global = fakeContext(t, "copy", []string{}, []string{"--dest-creds", ""}) - _, err = contextFromGlobalOptions(c, global, "dest-") + c, opts = fakeContext(t, "copy", "dest-", []string{}, []string{"--dest-creds", ""}) + _, err = contextFromImageOptions(c, opts, "dest-") assert.Error(t, err) } From 09a120a59bee9fecb011102e82a651d3c438ccc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Jul 2018 04:57:21 +0200 Subject: [PATCH 13/30] Temporarily add flagPrefix to imageOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don't want to worry about mismatch of the flagPrefix value between imageFlags() and contextFromImageOptions(). For now, record it in imageOptions; eventually we will stop using it in contextFromImageOptions and remove it again. Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 4 ++-- cmd/skopeo/delete.go | 2 +- cmd/skopeo/inspect.go | 2 +- cmd/skopeo/layers.go | 2 +- cmd/skopeo/utils.go | 36 ++++++++++++++++++++---------------- cmd/skopeo/utils_test.go | 10 +++++----- 6 files changed, 30 insertions(+), 26 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index fdff7c1b..57c567f2 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -18,12 +18,12 @@ import ( // contextsFromCopyOptions returns source and destionation types.SystemContext depending on c. func contextsFromCopyOptions(c *cli.Context, opts *copyOptions) (*types.SystemContext, *types.SystemContext, error) { - sourceCtx, err := contextFromImageOptions(c, opts.srcImage, "src-") + sourceCtx, err := contextFromImageOptions(c, opts.srcImage) if err != nil { return nil, nil, err } - destinationCtx, err := contextFromImageOptions(c, opts.destImage, "dest-") + destinationCtx, err := contextFromImageOptions(c, opts.destImage) if err != nil { return nil, nil, err } diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go index 5a9de127..f1665e92 100644 --- a/cmd/skopeo/delete.go +++ b/cmd/skopeo/delete.go @@ -67,7 +67,7 @@ func (opts *deleteOptions) run(c *cli.Context) error { return fmt.Errorf("Invalid source name %s: %v", c.Args()[0], err) } - sys, err := contextFromImageOptions(c, opts.image, "") + sys, err := contextFromImageOptions(c, opts.image) if err != nil { return err } diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index d35b4a75..2b81ca3c 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -132,7 +132,7 @@ func (opts *inspectOptions) run(c *cli.Context) (retErr error) { outputData.Name = dockerRef.Name() } if img.Reference().Transport() == docker.Transport { - sys, err := contextFromImageOptions(c, opts.image, "") + sys, err := contextFromImageOptions(c, opts.image) if err != nil { return err } diff --git a/cmd/skopeo/layers.go b/cmd/skopeo/layers.go index da04b625..fa1714f1 100644 --- a/cmd/skopeo/layers.go +++ b/cmd/skopeo/layers.go @@ -45,7 +45,7 @@ func (opts *layersOptions) run(c *cli.Context) (retErr error) { ctx, cancel := opts.global.commandTimeoutContext() defer cancel() - sys, err := contextFromImageOptions(c, opts.image, "") + sys, err := contextFromImageOptions(c, opts.image) if err != nil { return err } diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index 79b0a182..d50b543b 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -13,39 +13,43 @@ import ( // imageOptions collects CLI flags which are the same across subcommands, but may be different for each image // (e.g. may differ between the source and destination of a copy) type imageOptions struct { - global *globalOptions // May be shared across several imageOptions instances. + global *globalOptions // May be shared across several imageOptions instances. + flagPrefix string // FIXME: Drop this eventually. } // imageFlags prepares a collection of CLI flags writing into imageOptions, and the managed imageOptions structure. func imageFlags(global *globalOptions, flagPrefix string) ([]cli.Flag, *imageOptions) { - opts := imageOptions{global: global} + opts := imageOptions{ + global: global, + flagPrefix: flagPrefix, + } return []cli.Flag{}, &opts } -func contextFromImageOptions(c *cli.Context, opts *imageOptions, flagPrefix string) (*types.SystemContext, error) { +func contextFromImageOptions(c *cli.Context, opts *imageOptions) (*types.SystemContext, error) { ctx := &types.SystemContext{ RegistriesDirPath: opts.global.registriesDirPath, ArchitectureChoice: opts.global.overrideArch, OSChoice: opts.global.overrideOS, - DockerCertPath: c.String(flagPrefix + "cert-dir"), - OSTreeTmpDirPath: c.String(flagPrefix + "ostree-tmp-dir"), - OCISharedBlobDirPath: c.String(flagPrefix + "shared-blob-dir"), - DirForceCompress: c.Bool(flagPrefix + "compress"), + DockerCertPath: c.String(opts.flagPrefix + "cert-dir"), + OSTreeTmpDirPath: c.String(opts.flagPrefix + "ostree-tmp-dir"), + OCISharedBlobDirPath: c.String(opts.flagPrefix + "shared-blob-dir"), + DirForceCompress: c.Bool(opts.flagPrefix + "compress"), AuthFilePath: c.String("authfile"), - DockerDaemonHost: c.String(flagPrefix + "daemon-host"), - DockerDaemonCertPath: c.String(flagPrefix + "cert-dir"), - DockerDaemonInsecureSkipTLSVerify: !c.BoolT(flagPrefix + "tls-verify"), + DockerDaemonHost: c.String(opts.flagPrefix + "daemon-host"), + DockerDaemonCertPath: c.String(opts.flagPrefix + "cert-dir"), + DockerDaemonInsecureSkipTLSVerify: !c.BoolT(opts.flagPrefix + "tls-verify"), } // DEPRECATED: We support this for backward compatibility, but override it if a per-image flag is provided. if opts.global.tlsVerify.present { ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!opts.global.tlsVerify.value) } - if c.IsSet(flagPrefix + "tls-verify") { - ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!c.BoolT(flagPrefix + "tls-verify")) + if c.IsSet(opts.flagPrefix + "tls-verify") { + ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!c.BoolT(opts.flagPrefix + "tls-verify")) } - if c.IsSet(flagPrefix + "creds") { + if c.IsSet(opts.flagPrefix + "creds") { var err error - ctx.DockerAuthConfig, err = getDockerAuth(c.String(flagPrefix + "creds")) + ctx.DockerAuthConfig, err = getDockerAuth(c.String(opts.flagPrefix + "creds")) if err != nil { return nil, err } @@ -86,7 +90,7 @@ func parseImage(ctx context.Context, c *cli.Context, opts *imageOptions) (types. if err != nil { return nil, err } - sys, err := contextFromImageOptions(c, opts, "") + sys, err := contextFromImageOptions(c, opts) if err != nil { return nil, err } @@ -100,7 +104,7 @@ func parseImageSource(ctx context.Context, c *cli.Context, opts *imageOptions, n if err != nil { return nil, err } - sys, err := contextFromImageOptions(c, opts, "") + sys, err := contextFromImageOptions(c, opts) if err != nil { return nil, err } diff --git a/cmd/skopeo/utils_test.go b/cmd/skopeo/utils_test.go index 44e61d93..dcceb9df 100644 --- a/cmd/skopeo/utils_test.go +++ b/cmd/skopeo/utils_test.go @@ -60,7 +60,7 @@ func TestContextFromImageOptions(t *testing.T) { // Default state c, opts := fakeContext(t, "copy", "dest-", []string{}, []string{}) - res, err := contextFromImageOptions(c, opts, "dest-") + res, err := contextFromImageOptions(c, opts) require.NoError(t, err) assert.Equal(t, &types.SystemContext{}, res) @@ -68,7 +68,7 @@ func TestContextFromImageOptions(t *testing.T) { c, opts = fakeContext(t, "copy", "dest-", []string{}, []string{ "--dest-compress=false", }) - res, err = contextFromImageOptions(c, opts, "dest-") + res, err = contextFromImageOptions(c, opts) require.NoError(t, err) assert.Equal(t, &types.SystemContext{}, res) @@ -87,7 +87,7 @@ func TestContextFromImageOptions(t *testing.T) { "--dest-tls-verify=false", "--dest-creds", "creds-user:creds-password", }) - res, err = contextFromImageOptions(c, opts, "dest-") + res, err = contextFromImageOptions(c, opts) require.NoError(t, err) assert.Equal(t, &types.SystemContext{ RegistriesDirPath: "/srv/registries.d", @@ -130,7 +130,7 @@ func TestContextFromImageOptions(t *testing.T) { cmdFlags = append(cmdFlags, "--dest-tls-verify="+c.cmd) } ctx, opts := fakeContext(t, "copy", "dest-", globalFlags, cmdFlags) - res, err = contextFromImageOptions(ctx, opts, "dest-") + res, err = contextFromImageOptions(ctx, opts) require.NoError(t, err) assert.Equal(t, c.expectedDocker, res.DockerInsecureSkipTLSVerify, "%#v", c) assert.Equal(t, c.expectedDockerDaemon, res.DockerDaemonInsecureSkipTLSVerify, "%#v", c) @@ -138,6 +138,6 @@ func TestContextFromImageOptions(t *testing.T) { // Invalid option values c, opts = fakeContext(t, "copy", "dest-", []string{}, []string{"--dest-creds", ""}) - _, err = contextFromImageOptions(c, opts, "dest-") + _, err = contextFromImageOptions(c, opts) assert.Error(t, err) } From 8b8afe0fda741ec62ac9affe048d2332521f7ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Wed, 18 Jul 2018 00:10:15 +0200 Subject: [PATCH 14/30] Migrate --*creds to imageOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is one of the ugliest parts; we need an extra parameter to support the irregular screds/dcreds aliases. This was previously unsupported by (skopeo layers). Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 14 ++------------ cmd/skopeo/delete.go | 7 +------ cmd/skopeo/inspect.go | 7 +------ cmd/skopeo/layers.go | 2 +- cmd/skopeo/utils.go | 27 +++++++++++++++++++++------ cmd/skopeo/utils_test.go | 2 +- 6 files changed, 27 insertions(+), 32 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 57c567f2..27293e74 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -42,8 +42,8 @@ type copyOptions struct { } func copyCmd(global *globalOptions) cli.Command { - srcFlags, srcOpts := imageFlags(global, "src-") - destFlags, destOpts := imageFlags(global, "dest-") + srcFlags, srcOpts := imageFlags(global, "src-", "screds") + destFlags, destOpts := imageFlags(global, "dest-", "dcreds") opts := copyOptions{global: global, srcImage: srcOpts, destImage: destOpts, @@ -84,16 +84,6 @@ func copyCmd(global *globalOptions) cli.Command { Usage: "Sign the image using a GPG key with the specified `FINGERPRINT`", Destination: &opts.signByFingerprint, }, - cli.StringFlag{ - Name: "src-creds, screds", - Value: "", - Usage: "Use `USERNAME[:PASSWORD]` for accessing the source registry", - }, - cli.StringFlag{ - Name: "dest-creds, dcreds", - Value: "", - Usage: "Use `USERNAME[:PASSWORD]` for accessing the destination registry", - }, cli.StringFlag{ Name: "src-cert-dir", Value: "", diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go index f1665e92..d520f8b5 100644 --- a/cmd/skopeo/delete.go +++ b/cmd/skopeo/delete.go @@ -16,7 +16,7 @@ type deleteOptions struct { } func deleteCmd(global *globalOptions) cli.Command { - imageFlags, imageOpts := imageFlags(global, "") + imageFlags, imageOpts := imageFlags(global, "", "") opts := deleteOptions{ global: global, image: imageOpts, @@ -39,11 +39,6 @@ func deleteCmd(global *globalOptions) cli.Command { Name: "authfile", Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", }, - cli.StringFlag{ - Name: "creds", - Value: "", - Usage: "Use `USERNAME[:PASSWORD]` for accessing the registry", - }, cli.StringFlag{ Name: "cert-dir", Value: "", diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 2b81ca3c..6c433919 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -36,7 +36,7 @@ type inspectOptions struct { } func inspectCmd(global *globalOptions) cli.Command { - imageFlags, imageOpts := imageFlags(global, "") + imageFlags, imageOpts := imageFlags(global, "", "") opts := inspectOptions{ global: global, image: imageOpts, @@ -72,11 +72,6 @@ func inspectCmd(global *globalOptions) cli.Command { Usage: "output raw manifest", Destination: &opts.raw, }, - cli.StringFlag{ - Name: "creds", - Value: "", - Usage: "Use `USERNAME[:PASSWORD]` for accessing the registry", - }, }, imageFlags...), Action: opts.run, } diff --git a/cmd/skopeo/layers.go b/cmd/skopeo/layers.go index fa1714f1..e4c53518 100644 --- a/cmd/skopeo/layers.go +++ b/cmd/skopeo/layers.go @@ -21,7 +21,7 @@ type layersOptions struct { } func layersCmd(global *globalOptions) cli.Command { - imageFlags, imageOpts := imageFlags(global, "") + imageFlags, imageOpts := imageFlags(global, "", "") opts := layersOptions{ global: global, image: imageOpts, diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index d50b543b..32a664ff 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -13,17 +13,32 @@ import ( // imageOptions collects CLI flags which are the same across subcommands, but may be different for each image // (e.g. may differ between the source and destination of a copy) type imageOptions struct { - global *globalOptions // May be shared across several imageOptions instances. - flagPrefix string // FIXME: Drop this eventually. + global *globalOptions // May be shared across several imageOptions instances. + flagPrefix string // FIXME: Drop this eventually. + credsOption optionalString // username[:password] for accessing a registry } // imageFlags prepares a collection of CLI flags writing into imageOptions, and the managed imageOptions structure. -func imageFlags(global *globalOptions, flagPrefix string) ([]cli.Flag, *imageOptions) { +func imageFlags(global *globalOptions, flagPrefix, credsOptionAlias string) ([]cli.Flag, *imageOptions) { opts := imageOptions{ global: global, flagPrefix: flagPrefix, } - return []cli.Flag{}, &opts + + // This is horribly ugly, but we need to support the old option forms of (skopeo copy) for compatibility. + // Don't add any more cases like this. + credsOptionExtra := "" + if credsOptionAlias != "" { + credsOptionExtra += "," + credsOptionAlias + } + + return []cli.Flag{ + cli.GenericFlag{ + Name: flagPrefix + "creds" + credsOptionExtra, + Usage: "Use `USERNAME[:PASSWORD]` for accessing the registry", + Value: newOptionalStringValue(&opts.credsOption), + }, + }, &opts } func contextFromImageOptions(c *cli.Context, opts *imageOptions) (*types.SystemContext, error) { @@ -47,9 +62,9 @@ func contextFromImageOptions(c *cli.Context, opts *imageOptions) (*types.SystemC if c.IsSet(opts.flagPrefix + "tls-verify") { ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!c.BoolT(opts.flagPrefix + "tls-verify")) } - if c.IsSet(opts.flagPrefix + "creds") { + if opts.credsOption.present { var err error - ctx.DockerAuthConfig, err = getDockerAuth(c.String(opts.flagPrefix + "creds")) + ctx.DockerAuthConfig, err = getDockerAuth(opts.credsOption.value) if err != nil { return nil, err } diff --git a/cmd/skopeo/utils_test.go b/cmd/skopeo/utils_test.go index dcceb9df..bc6adfb2 100644 --- a/cmd/skopeo/utils_test.go +++ b/cmd/skopeo/utils_test.go @@ -27,7 +27,7 @@ func fakeContext(t *testing.T, cmdName string, flagPrefix string, globalFlags [] cmd := app.Command(cmdName) require.NotNil(t, cmd) - imageFlags, imageOpts := imageFlags(globalOpts, flagPrefix) + imageFlags, imageOpts := imageFlags(globalOpts, flagPrefix, "") appliedFlags := map[string]struct{}{} // Ugly: cmd.Flags includes imageFlags as well. For now, we need cmd.Flags to apply here // to be able to test the non-Destination: flags, but we must not apply the same flag name twice. From 1faff791ceb9b3091f81ad0ae8990a4b75b581b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Jul 2018 05:19:26 +0200 Subject: [PATCH 15/30] Migrate --*cert-dir to imageOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was previously unsupported by (skopeo layers). Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 10 ---------- cmd/skopeo/delete.go | 5 ----- cmd/skopeo/inspect.go | 5 ----- cmd/skopeo/utils.go | 16 +++++++++++----- 4 files changed, 11 insertions(+), 25 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 27293e74..7fa0c06e 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -84,20 +84,10 @@ func copyCmd(global *globalOptions) cli.Command { Usage: "Sign the image using a GPG key with the specified `FINGERPRINT`", Destination: &opts.signByFingerprint, }, - cli.StringFlag{ - Name: "src-cert-dir", - Value: "", - Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the source registry or daemon", - }, cli.BoolTFlag{ Name: "src-tls-verify", Usage: "require HTTPS and verify certificates when talking to the container source registry or daemon (defaults to true)", }, - cli.StringFlag{ - Name: "dest-cert-dir", - Value: "", - Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the destination registry or daemon", - }, cli.BoolTFlag{ Name: "dest-tls-verify", Usage: "require HTTPS and verify certificates when talking to the container destination registry or daemon (defaults to true)", diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go index d520f8b5..cd321e70 100644 --- a/cmd/skopeo/delete.go +++ b/cmd/skopeo/delete.go @@ -39,11 +39,6 @@ func deleteCmd(global *globalOptions) cli.Command { Name: "authfile", Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", }, - cli.StringFlag{ - Name: "cert-dir", - Value: "", - Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the registry", - }, cli.BoolTFlag{ Name: "tls-verify", Usage: "require HTTPS and verify certificates when talking to container registries (defaults to true)", diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 6c433919..78085615 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -58,11 +58,6 @@ func inspectCmd(global *globalOptions) cli.Command { Name: "authfile", Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", }, - cli.StringFlag{ - Name: "cert-dir", - Value: "", - Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the registry", - }, cli.BoolTFlag{ Name: "tls-verify", Usage: "require HTTPS and verify certificates when talking to container registries (defaults to true)", diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index 32a664ff..d298689e 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -13,9 +13,10 @@ import ( // imageOptions collects CLI flags which are the same across subcommands, but may be different for each image // (e.g. may differ between the source and destination of a copy) type imageOptions struct { - global *globalOptions // May be shared across several imageOptions instances. - flagPrefix string // FIXME: Drop this eventually. - credsOption optionalString // username[:password] for accessing a registry + global *globalOptions // May be shared across several imageOptions instances. + flagPrefix string // FIXME: Drop this eventually. + credsOption optionalString // username[:password] for accessing a registry + dockerCertPath string // A directory using Docker-like *.{crt,cert,key} files for connecting to a registry or a daemon } // imageFlags prepares a collection of CLI flags writing into imageOptions, and the managed imageOptions structure. @@ -38,6 +39,11 @@ func imageFlags(global *globalOptions, flagPrefix, credsOptionAlias string) ([]c Usage: "Use `USERNAME[:PASSWORD]` for accessing the registry", Value: newOptionalStringValue(&opts.credsOption), }, + cli.StringFlag{ + Name: flagPrefix + "cert-dir", + Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the registry or daemon", + Destination: &opts.dockerCertPath, + }, }, &opts } @@ -46,13 +52,13 @@ func contextFromImageOptions(c *cli.Context, opts *imageOptions) (*types.SystemC RegistriesDirPath: opts.global.registriesDirPath, ArchitectureChoice: opts.global.overrideArch, OSChoice: opts.global.overrideOS, - DockerCertPath: c.String(opts.flagPrefix + "cert-dir"), + DockerCertPath: opts.dockerCertPath, OSTreeTmpDirPath: c.String(opts.flagPrefix + "ostree-tmp-dir"), OCISharedBlobDirPath: c.String(opts.flagPrefix + "shared-blob-dir"), DirForceCompress: c.Bool(opts.flagPrefix + "compress"), AuthFilePath: c.String("authfile"), DockerDaemonHost: c.String(opts.flagPrefix + "daemon-host"), - DockerDaemonCertPath: c.String(opts.flagPrefix + "cert-dir"), + DockerDaemonCertPath: opts.dockerCertPath, DockerDaemonInsecureSkipTLSVerify: !c.BoolT(opts.flagPrefix + "tls-verify"), } // DEPRECATED: We support this for backward compatibility, but override it if a per-image flag is provided. From a77b409619e3b89762746bf454e95f615a454524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Jul 2018 05:28:35 +0200 Subject: [PATCH 16/30] Migrate --*tls-verify to imageOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was previously unsupported by (skopeo layers) Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 8 -------- cmd/skopeo/delete.go | 4 ---- cmd/skopeo/inspect.go | 4 ---- cmd/skopeo/utils.go | 34 +++++++++++++++++++++------------- cmd/skopeo/utils_test.go | 1 - 5 files changed, 21 insertions(+), 30 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 7fa0c06e..5ebbd4ca 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -84,14 +84,6 @@ func copyCmd(global *globalOptions) cli.Command { Usage: "Sign the image using a GPG key with the specified `FINGERPRINT`", Destination: &opts.signByFingerprint, }, - cli.BoolTFlag{ - Name: "src-tls-verify", - Usage: "require HTTPS and verify certificates when talking to the container source registry or daemon (defaults to true)", - }, - cli.BoolTFlag{ - Name: "dest-tls-verify", - Usage: "require HTTPS and verify certificates when talking to the container destination registry or daemon (defaults to true)", - }, cli.StringFlag{ Name: "dest-ostree-tmp-dir", Value: "", diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go index cd321e70..a7d4f0d1 100644 --- a/cmd/skopeo/delete.go +++ b/cmd/skopeo/delete.go @@ -39,10 +39,6 @@ func deleteCmd(global *globalOptions) cli.Command { Name: "authfile", Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", }, - cli.BoolTFlag{ - Name: "tls-verify", - Usage: "require HTTPS and verify certificates when talking to container registries (defaults to true)", - }, }, imageFlags...), } } diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 78085615..0bd7c928 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -58,10 +58,6 @@ func inspectCmd(global *globalOptions) cli.Command { Name: "authfile", Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", }, - cli.BoolTFlag{ - Name: "tls-verify", - Usage: "require HTTPS and verify certificates when talking to container registries (defaults to true)", - }, cli.BoolFlag{ Name: "raw", Usage: "output raw manifest", diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index d298689e..0b16d730 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -17,6 +17,7 @@ type imageOptions struct { flagPrefix string // FIXME: Drop this eventually. credsOption optionalString // username[:password] for accessing a registry dockerCertPath string // A directory using Docker-like *.{crt,cert,key} files for connecting to a registry or a daemon + tlsVerify optionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:) } // imageFlags prepares a collection of CLI flags writing into imageOptions, and the managed imageOptions structure. @@ -44,29 +45,36 @@ func imageFlags(global *globalOptions, flagPrefix, credsOptionAlias string) ([]c Usage: "use certificates at `PATH` (*.crt, *.cert, *.key) to connect to the registry or daemon", Destination: &opts.dockerCertPath, }, + cli.GenericFlag{ + Name: flagPrefix + "tls-verify", + Usage: "require HTTPS and verify certificates when talking to the container registry or daemon (defaults to true)", + Value: newOptionalBoolValue(&opts.tlsVerify), + }, }, &opts } func contextFromImageOptions(c *cli.Context, opts *imageOptions) (*types.SystemContext, error) { ctx := &types.SystemContext{ - RegistriesDirPath: opts.global.registriesDirPath, - ArchitectureChoice: opts.global.overrideArch, - OSChoice: opts.global.overrideOS, - DockerCertPath: opts.dockerCertPath, - OSTreeTmpDirPath: c.String(opts.flagPrefix + "ostree-tmp-dir"), - OCISharedBlobDirPath: c.String(opts.flagPrefix + "shared-blob-dir"), - DirForceCompress: c.Bool(opts.flagPrefix + "compress"), - AuthFilePath: c.String("authfile"), - DockerDaemonHost: c.String(opts.flagPrefix + "daemon-host"), - DockerDaemonCertPath: opts.dockerCertPath, - DockerDaemonInsecureSkipTLSVerify: !c.BoolT(opts.flagPrefix + "tls-verify"), + RegistriesDirPath: opts.global.registriesDirPath, + ArchitectureChoice: opts.global.overrideArch, + OSChoice: opts.global.overrideOS, + DockerCertPath: opts.dockerCertPath, + OSTreeTmpDirPath: c.String(opts.flagPrefix + "ostree-tmp-dir"), + OCISharedBlobDirPath: c.String(opts.flagPrefix + "shared-blob-dir"), + DirForceCompress: c.Bool(opts.flagPrefix + "compress"), + AuthFilePath: c.String("authfile"), + DockerDaemonHost: c.String(opts.flagPrefix + "daemon-host"), + DockerDaemonCertPath: opts.dockerCertPath, + } + if opts.tlsVerify.present { + ctx.DockerDaemonInsecureSkipTLSVerify = !opts.tlsVerify.value } // DEPRECATED: We support this for backward compatibility, but override it if a per-image flag is provided. if opts.global.tlsVerify.present { ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!opts.global.tlsVerify.value) } - if c.IsSet(opts.flagPrefix + "tls-verify") { - ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!c.BoolT(opts.flagPrefix + "tls-verify")) + if opts.tlsVerify.present { + ctx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!opts.tlsVerify.value) } if opts.credsOption.present { var err error diff --git a/cmd/skopeo/utils_test.go b/cmd/skopeo/utils_test.go index bc6adfb2..6aeb66ec 100644 --- a/cmd/skopeo/utils_test.go +++ b/cmd/skopeo/utils_test.go @@ -56,7 +56,6 @@ func fakeContext(t *testing.T, cmdName string, flagPrefix string, globalFlags [] func TestContextFromImageOptions(t *testing.T) { // FIXME: All of this only tests (skopeo copy --dest) - // FIXME FIXME: Apparently BoolT values are set to false if the flag is not declared for the specific subcommand!! // Default state c, opts := fakeContext(t, "copy", "dest-", []string{}, []string{}) From 694f915003c95174e2e9ee5828f0a6d3409a6d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Jul 2018 05:32:20 +0200 Subject: [PATCH 17/30] Migrate --*shared-blob-dir to imageOptions. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was previously only supported in (skopeo copy). Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 10 ---------- cmd/skopeo/utils.go | 8 +++++++- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 5ebbd4ca..28e8a3c0 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -89,16 +89,6 @@ func copyCmd(global *globalOptions) cli.Command { Value: "", Usage: "`DIRECTORY` to use for OSTree temporary files", }, - cli.StringFlag{ - Name: "src-shared-blob-dir", - Value: "", - Usage: "`DIRECTORY` to use to fetch retrieved blobs (OCI layout sources only)", - }, - cli.StringFlag{ - Name: "dest-shared-blob-dir", - Value: "", - Usage: "`DIRECTORY` to use to store retrieved blobs (OCI layout destinations only)", - }, cli.GenericFlag{ Name: "format, f", Usage: "`MANIFEST TYPE` (oci, v2s1, or v2s2) to use when saving image to directory using the 'dir:' transport (default is manifest type of source)", diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index 0b16d730..eddacdd9 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -18,6 +18,7 @@ type imageOptions struct { credsOption optionalString // username[:password] for accessing a registry dockerCertPath string // A directory using Docker-like *.{crt,cert,key} files for connecting to a registry or a daemon tlsVerify optionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:) + sharedBlobDir string // A directory to use for OCI blobs, shared across repositories } // imageFlags prepares a collection of CLI flags writing into imageOptions, and the managed imageOptions structure. @@ -50,6 +51,11 @@ func imageFlags(global *globalOptions, flagPrefix, credsOptionAlias string) ([]c Usage: "require HTTPS and verify certificates when talking to the container registry or daemon (defaults to true)", Value: newOptionalBoolValue(&opts.tlsVerify), }, + cli.StringFlag{ + Name: flagPrefix + "shared-blob-dir", + Usage: "`DIRECTORY` to use to share blobs across OCI repositories", + Destination: &opts.sharedBlobDir, + }, }, &opts } @@ -60,7 +66,7 @@ func contextFromImageOptions(c *cli.Context, opts *imageOptions) (*types.SystemC OSChoice: opts.global.overrideOS, DockerCertPath: opts.dockerCertPath, OSTreeTmpDirPath: c.String(opts.flagPrefix + "ostree-tmp-dir"), - OCISharedBlobDirPath: c.String(opts.flagPrefix + "shared-blob-dir"), + OCISharedBlobDirPath: opts.sharedBlobDir, DirForceCompress: c.Bool(opts.flagPrefix + "compress"), AuthFilePath: c.String("authfile"), DockerDaemonHost: c.String(opts.flagPrefix + "daemon-host"), From 7e8c89d619aaf4f12cc61a89114539952195af27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Sat, 7 Jul 2018 05:34:26 +0200 Subject: [PATCH 18/30] Migrate --*daemon-host to imageOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was previously only supported in (skopeo copy). Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 10 ---------- cmd/skopeo/utils.go | 20 +++++++++++++------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 28e8a3c0..274cfb4c 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -98,16 +98,6 @@ func copyCmd(global *globalOptions) cli.Command { Name: "dest-compress", Usage: "Compress tarball image layers when saving to directory using the 'dir' transport. (default is same compression type as source)", }, - cli.StringFlag{ - Name: "src-daemon-host", - Value: "", - Usage: "use docker daemon host at `HOST` (docker-daemon sources only)", - }, - cli.StringFlag{ - Name: "dest-daemon-host", - Value: "", - Usage: "use docker daemon host at `HOST` (docker-daemon destinations only)", - }, }, srcFlags...), destFlags...), } } diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index eddacdd9..d8c71c48 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -13,12 +13,13 @@ import ( // imageOptions collects CLI flags which are the same across subcommands, but may be different for each image // (e.g. may differ between the source and destination of a copy) type imageOptions struct { - global *globalOptions // May be shared across several imageOptions instances. - flagPrefix string // FIXME: Drop this eventually. - credsOption optionalString // username[:password] for accessing a registry - dockerCertPath string // A directory using Docker-like *.{crt,cert,key} files for connecting to a registry or a daemon - tlsVerify optionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:) - sharedBlobDir string // A directory to use for OCI blobs, shared across repositories + global *globalOptions // May be shared across several imageOptions instances. + flagPrefix string // FIXME: Drop this eventually. + credsOption optionalString // username[:password] for accessing a registry + dockerCertPath string // A directory using Docker-like *.{crt,cert,key} files for connecting to a registry or a daemon + tlsVerify optionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:) + sharedBlobDir string // A directory to use for OCI blobs, shared across repositories + dockerDaemonHost string // docker-daemon: host to connect to } // imageFlags prepares a collection of CLI flags writing into imageOptions, and the managed imageOptions structure. @@ -56,6 +57,11 @@ func imageFlags(global *globalOptions, flagPrefix, credsOptionAlias string) ([]c Usage: "`DIRECTORY` to use to share blobs across OCI repositories", Destination: &opts.sharedBlobDir, }, + cli.StringFlag{ + Name: flagPrefix + "daemon-host", + Usage: "use docker daemon host at `HOST` (docker-daemon: only)", + Destination: &opts.dockerDaemonHost, + }, }, &opts } @@ -69,7 +75,7 @@ func contextFromImageOptions(c *cli.Context, opts *imageOptions) (*types.SystemC OCISharedBlobDirPath: opts.sharedBlobDir, DirForceCompress: c.Bool(opts.flagPrefix + "compress"), AuthFilePath: c.String("authfile"), - DockerDaemonHost: c.String(opts.flagPrefix + "daemon-host"), + DockerDaemonHost: opts.dockerDaemonHost, DockerDaemonCertPath: opts.dockerCertPath, } if opts.tlsVerify.present { From 88c748f47a3d2b8321d94ccd7110c5a41792a669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Mon, 9 Jul 2018 20:40:15 +0200 Subject: [PATCH 19/30] Introduce imageDestOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is an extension of imageOptions that carries destination-specific flags. This will allow us to handle --dest-* flags without also exposing pointless --src-* flags. (This is, also, where the type-safety somewhat breaks down; after all the work to make the data flow and availability explicit, everything ends up in an types.SystemContext, and it's easy enough to use a destination-specific one for sources. OTOH, this is not making the situation worse in any sense.) Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 6 +- cmd/skopeo/utils.go | 22 +++++++ cmd/skopeo/utils_test.go | 133 ++++++++++++++++++++++++++++++++++----- 3 files changed, 141 insertions(+), 20 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 274cfb4c..604ddff1 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -23,7 +23,7 @@ func contextsFromCopyOptions(c *cli.Context, opts *copyOptions) (*types.SystemCo return nil, nil, err } - destinationCtx, err := contextFromImageOptions(c, opts.destImage) + destinationCtx, err := contextFromImageDestOptions(c, opts.destImage) if err != nil { return nil, nil, err } @@ -34,7 +34,7 @@ func contextsFromCopyOptions(c *cli.Context, opts *copyOptions) (*types.SystemCo type copyOptions struct { global *globalOptions srcImage *imageOptions - destImage *imageOptions + destImage *imageDestOptions additionalTags cli.StringSlice // For docker-archive: destinations, in addition to the name:tag specified as destination, also add these removeSignatures bool // Do not copy signatures from the source image signByFingerprint string // Sign the image using a GPG key with the specified fingerprint @@ -43,7 +43,7 @@ type copyOptions struct { func copyCmd(global *globalOptions) cli.Command { srcFlags, srcOpts := imageFlags(global, "src-", "screds") - destFlags, destOpts := imageFlags(global, "dest-", "dcreds") + destFlags, destOpts := imageDestFlags(global, "dest-", "dcreds") opts := copyOptions{global: global, srcImage: srcOpts, destImage: destOpts, diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index d8c71c48..8bddfb0d 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -98,6 +98,28 @@ func contextFromImageOptions(c *cli.Context, opts *imageOptions) (*types.SystemC return ctx, nil } +// imageDestOptions is a superset of imageOptions specialized for iamge destinations. +type imageDestOptions struct { + *imageOptions +} + +// imageDestFlags prepares a collection of CLI flags writing into imageDestOptions, and the managed imageDestOptions structure. +func imageDestFlags(global *globalOptions, flagPrefix, credsOptionAlias string) ([]cli.Flag, *imageDestOptions) { + genericFlags, genericOptions := imageFlags(global, flagPrefix, credsOptionAlias) + opts := imageDestOptions{imageOptions: genericOptions} + + return append(genericFlags, []cli.Flag{}...), &opts +} + +func contextFromImageDestOptions(c *cli.Context, opts *imageDestOptions) (*types.SystemContext, error) { + ctx, err := contextFromImageOptions(c, opts.imageOptions) + if err != nil { + return nil, err + } + + return ctx, err +} + func parseCreds(creds string) (string, string, error) { if creds == "" { return "", "", errors.New("credentials can't be empty") diff --git a/cmd/skopeo/utils_test.go b/cmd/skopeo/utils_test.go index 6aeb66ec..2447f79c 100644 --- a/cmd/skopeo/utils_test.go +++ b/cmd/skopeo/utils_test.go @@ -11,18 +11,26 @@ import ( "github.com/urfave/cli" ) -// fakeContext creates inputs for contextFromImageOptions. +// fakeGlobalOptions creates globalOptions and sets it according to flags. // NOTE: This is QUITE FAKE; none of the urfave/cli normalization and the like happens. -func fakeContext(t *testing.T, cmdName string, flagPrefix string, globalFlags []string, cmdFlags []string) (*cli.Context, *imageOptions) { - app, globalOpts := createApp() +func fakeGlobalOptions(t *testing.T, flags []string) (*cli.App, *cli.Context, *globalOptions) { + app, opts := createApp() - globalSet := flag.NewFlagSet(app.Name, flag.ContinueOnError) + flagSet := flag.NewFlagSet(app.Name, flag.ContinueOnError) for _, f := range app.Flags { - f.Apply(globalSet) + f.Apply(flagSet) } - err := globalSet.Parse(globalFlags) + err := flagSet.Parse(flags) require.NoError(t, err) - globalCtx := cli.NewContext(app, globalSet, nil) + ctx := cli.NewContext(app, flagSet, nil) + + return app, ctx, opts +} + +// fakeImageContext creates inputs for contextFromImageOptions. +// NOTE: This is QUITE FAKE; none of the urfave/cli normalization and the like happens. +func fakeImageContext(t *testing.T, cmdName string, flagPrefix string, globalFlags []string, cmdFlags []string) (*cli.Context, *imageOptions) { + app, globalCtx, globalOpts := fakeGlobalOptions(t, globalFlags) cmd := app.Command(cmdName) require.NotNil(t, cmd) @@ -37,34 +45,34 @@ func fakeContext(t *testing.T, cmdName string, flagPrefix string, globalFlags [] firstName := func(f cli.Flag) string { // We even need to recognize "dest-creds,dcreds". This will disappear as well. return strings.Split(f.GetName(), ",")[0] } - cmdSet := flag.NewFlagSet(cmd.Name, flag.ContinueOnError) + flagSet := flag.NewFlagSet(cmd.Name, flag.ContinueOnError) for _, f := range imageFlags { - f.Apply(cmdSet) + f.Apply(flagSet) appliedFlags[firstName(f)] = struct{}{} } for _, f := range cmd.Flags { if _, ok := appliedFlags[firstName(f)]; !ok { - f.Apply(cmdSet) + f.Apply(flagSet) appliedFlags[firstName(f)] = struct{}{} } } - err = cmdSet.Parse(cmdFlags) + err := flagSet.Parse(cmdFlags) require.NoError(t, err) - return cli.NewContext(app, cmdSet, globalCtx), imageOpts + return cli.NewContext(app, flagSet, globalCtx), imageOpts } func TestContextFromImageOptions(t *testing.T) { // FIXME: All of this only tests (skopeo copy --dest) // Default state - c, opts := fakeContext(t, "copy", "dest-", []string{}, []string{}) + c, opts := fakeImageContext(t, "copy", "dest-", []string{}, []string{}) res, err := contextFromImageOptions(c, opts) require.NoError(t, err) assert.Equal(t, &types.SystemContext{}, res) // Explicitly set everything to default, except for when the default is “not present” - c, opts = fakeContext(t, "copy", "dest-", []string{}, []string{ + c, opts = fakeImageContext(t, "copy", "dest-", []string{}, []string{ "--dest-compress=false", }) res, err = contextFromImageOptions(c, opts) @@ -72,7 +80,7 @@ func TestContextFromImageOptions(t *testing.T) { assert.Equal(t, &types.SystemContext{}, res) // Set everything to non-default values. - c, opts = fakeContext(t, "copy", "dest-", []string{ + c, opts = fakeImageContext(t, "copy", "dest-", []string{ "--registries.d", "/srv/registries.d", "--override-arch", "overridden-arch", "--override-os", "overridden-os", @@ -128,7 +136,7 @@ func TestContextFromImageOptions(t *testing.T) { if c.cmd != "" { cmdFlags = append(cmdFlags, "--dest-tls-verify="+c.cmd) } - ctx, opts := fakeContext(t, "copy", "dest-", globalFlags, cmdFlags) + ctx, opts := fakeImageContext(t, "copy", "dest-", globalFlags, cmdFlags) res, err = contextFromImageOptions(ctx, opts) require.NoError(t, err) assert.Equal(t, c.expectedDocker, res.DockerInsecureSkipTLSVerify, "%#v", c) @@ -136,7 +144,98 @@ func TestContextFromImageOptions(t *testing.T) { } // Invalid option values - c, opts = fakeContext(t, "copy", "dest-", []string{}, []string{"--dest-creds", ""}) + c, opts = fakeImageContext(t, "copy", "dest-", []string{}, []string{"--dest-creds", ""}) _, err = contextFromImageOptions(c, opts) assert.Error(t, err) } + +// fakeImageDestContext creates inputs for contextFromImageDestOptions. +// NOTE: This is QUITE FAKE; none of the urfave/cli normalization and the like happens. +func fakeImageDestContext(t *testing.T, cmdName string, flagPrefix string, globalFlags []string, cmdFlags []string) (*cli.Context, *imageDestOptions) { + app, globalCtx, globalOpts := fakeGlobalOptions(t, globalFlags) + + cmd := app.Command(cmdName) + require.NotNil(t, cmd) + + imageFlags, imageOpts := imageDestFlags(globalOpts, flagPrefix, "") + appliedFlags := map[string]struct{}{} + // Ugly: cmd.Flags includes imageFlags as well. For now, we need cmd.Flags to apply here + // to be able to test the non-Destination: flags, but we must not apply the same flag name twice. + // So, primarily use imageFlags (so that Destination: is used as expected), and then follow up with + // the remaining flags from cmd.Flags (so that cli.Context.String() etc. works). + // This is horribly ugly, but all of this will disappear within this PR. + firstName := func(f cli.Flag) string { // We even need to recognize "dest-creds,dcreds". This will disappear as well. + return strings.Split(f.GetName(), ",")[0] + } + flagSet := flag.NewFlagSet(cmd.Name, flag.ContinueOnError) + for _, f := range imageFlags { + f.Apply(flagSet) + appliedFlags[firstName(f)] = struct{}{} + } + for _, f := range cmd.Flags { + if _, ok := appliedFlags[firstName(f)]; !ok { + f.Apply(flagSet) + appliedFlags[firstName(f)] = struct{}{} + } + } + + err := flagSet.Parse(cmdFlags) + require.NoError(t, err) + return cli.NewContext(app, flagSet, globalCtx), imageOpts +} + +func TestContextFromImageDestOptions(t *testing.T) { + // FIXME: All of this only tests (skopeo copy --dest) + + // Default state + c, opts := fakeImageDestContext(t, "copy", "dest-", []string{}, []string{}) + res, err := contextFromImageDestOptions(c, opts) + require.NoError(t, err) + assert.Equal(t, &types.SystemContext{}, res) + + // Explicitly set everything to default, except for when the default is “not present” + c, opts = fakeImageDestContext(t, "copy", "dest-", []string{}, []string{ + "--dest-compress=false", + }) + res, err = contextFromImageDestOptions(c, opts) + require.NoError(t, err) + assert.Equal(t, &types.SystemContext{}, res) + + // Set everything to non-default values. + c, opts = fakeImageDestContext(t, "copy", "dest-", []string{ + "--registries.d", "/srv/registries.d", + "--override-arch", "overridden-arch", + "--override-os", "overridden-os", + }, []string{ + "--authfile", "/srv/authfile", + "--dest-cert-dir", "/srv/cert-dir", + "--dest-ostree-tmp-dir", "/srv/ostree-tmp-dir", + "--dest-shared-blob-dir", "/srv/shared-blob-dir", + "--dest-compress=true", + "--dest-daemon-host", "daemon-host.example.com", + "--dest-tls-verify=false", + "--dest-creds", "creds-user:creds-password", + }) + res, err = contextFromImageDestOptions(c, opts) + require.NoError(t, err) + assert.Equal(t, &types.SystemContext{ + RegistriesDirPath: "/srv/registries.d", + AuthFilePath: "/srv/authfile", + ArchitectureChoice: "overridden-arch", + OSChoice: "overridden-os", + OCISharedBlobDirPath: "/srv/shared-blob-dir", + DockerCertPath: "/srv/cert-dir", + DockerInsecureSkipTLSVerify: types.OptionalBoolTrue, + DockerAuthConfig: &types.DockerAuthConfig{Username: "creds-user", Password: "creds-password"}, + OSTreeTmpDirPath: "/srv/ostree-tmp-dir", + DockerDaemonCertPath: "/srv/cert-dir", + DockerDaemonHost: "daemon-host.example.com", + DockerDaemonInsecureSkipTLSVerify: true, + DirForceCompress: true, + }, res) + + // Invalid option values in imageOptions + c, opts = fakeImageDestContext(t, "copy", "dest-", []string{}, []string{"--dest-creds", ""}) + _, err = contextFromImageDestOptions(c, opts) + assert.Error(t, err) +} From 72a3dc17eeb38a7048c9691df9ca0bf9ebfeb5c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Mon, 9 Jul 2018 20:51:00 +0200 Subject: [PATCH 20/30] Migrate --dest-ostree-tmp-dir to imageDestOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 5 ----- cmd/skopeo/utils.go | 15 +++++++++++++-- cmd/skopeo/utils_test.go | 2 -- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 604ddff1..e5d5b7d7 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -84,11 +84,6 @@ func copyCmd(global *globalOptions) cli.Command { Usage: "Sign the image using a GPG key with the specified `FINGERPRINT`", Destination: &opts.signByFingerprint, }, - cli.StringFlag{ - Name: "dest-ostree-tmp-dir", - Value: "", - Usage: "`DIRECTORY` to use for OSTree temporary files", - }, cli.GenericFlag{ Name: "format, f", Usage: "`MANIFEST TYPE` (oci, v2s1, or v2s2) to use when saving image to directory using the 'dir:' transport (default is manifest type of source)", diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index 8bddfb0d..26b16993 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -65,13 +65,14 @@ func imageFlags(global *globalOptions, flagPrefix, credsOptionAlias string) ([]c }, &opts } +// contextFromImageOptions returns a *types.SystemContext corresponding to opts. +// It is guaranteed to return a fresh instance, so it is safe to make additional updates to it. func contextFromImageOptions(c *cli.Context, opts *imageOptions) (*types.SystemContext, error) { ctx := &types.SystemContext{ RegistriesDirPath: opts.global.registriesDirPath, ArchitectureChoice: opts.global.overrideArch, OSChoice: opts.global.overrideOS, DockerCertPath: opts.dockerCertPath, - OSTreeTmpDirPath: c.String(opts.flagPrefix + "ostree-tmp-dir"), OCISharedBlobDirPath: opts.sharedBlobDir, DirForceCompress: c.Bool(opts.flagPrefix + "compress"), AuthFilePath: c.String("authfile"), @@ -101,6 +102,7 @@ func contextFromImageOptions(c *cli.Context, opts *imageOptions) (*types.SystemC // imageDestOptions is a superset of imageOptions specialized for iamge destinations. type imageDestOptions struct { *imageOptions + osTreeTmpDir string // A directory to use for OSTree temporary files } // imageDestFlags prepares a collection of CLI flags writing into imageDestOptions, and the managed imageDestOptions structure. @@ -108,15 +110,24 @@ func imageDestFlags(global *globalOptions, flagPrefix, credsOptionAlias string) genericFlags, genericOptions := imageFlags(global, flagPrefix, credsOptionAlias) opts := imageDestOptions{imageOptions: genericOptions} - return append(genericFlags, []cli.Flag{}...), &opts + return append(genericFlags, []cli.Flag{ + cli.StringFlag{ + Name: flagPrefix + "ostree-tmp-dir", + Usage: "`DIRECTORY` to use for OSTree temporary files", + Destination: &opts.osTreeTmpDir, + }, + }...), &opts } +// contextFromImageDestOptions returns a *types.SystemContext corresponding to opts. +// It is guaranteed to return a fresh instance, so it is safe to make additional updates to it. func contextFromImageDestOptions(c *cli.Context, opts *imageDestOptions) (*types.SystemContext, error) { ctx, err := contextFromImageOptions(c, opts.imageOptions) if err != nil { return nil, err } + ctx.OSTreeTmpDirPath = opts.osTreeTmpDir return ctx, err } diff --git a/cmd/skopeo/utils_test.go b/cmd/skopeo/utils_test.go index 2447f79c..3934c465 100644 --- a/cmd/skopeo/utils_test.go +++ b/cmd/skopeo/utils_test.go @@ -87,7 +87,6 @@ func TestContextFromImageOptions(t *testing.T) { }, []string{ "--authfile", "/srv/authfile", "--dest-cert-dir", "/srv/cert-dir", - "--dest-ostree-tmp-dir", "/srv/ostree-tmp-dir", "--dest-shared-blob-dir", "/srv/shared-blob-dir", "--dest-compress=true", "--dest-daemon-host", "daemon-host.example.com", @@ -105,7 +104,6 @@ func TestContextFromImageOptions(t *testing.T) { DockerCertPath: "/srv/cert-dir", DockerInsecureSkipTLSVerify: types.OptionalBoolTrue, DockerAuthConfig: &types.DockerAuthConfig{Username: "creds-user", Password: "creds-password"}, - OSTreeTmpDirPath: "/srv/ostree-tmp-dir", DockerDaemonCertPath: "/srv/cert-dir", DockerDaemonHost: "daemon-host.example.com", DockerDaemonInsecureSkipTLSVerify: true, From 444b90a9cfb1c025fd362d3e6f94f1ae5d17eb24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Mon, 9 Jul 2018 20:55:15 +0200 Subject: [PATCH 21/30] Migrate --dest-compress to imageDestOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 4 ---- cmd/skopeo/utils.go | 10 ++++++++-- cmd/skopeo/utils_test.go | 10 ---------- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index e5d5b7d7..f5b2e9c5 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -89,10 +89,6 @@ func copyCmd(global *globalOptions) cli.Command { Usage: "`MANIFEST TYPE` (oci, v2s1, or v2s2) to use when saving image to directory using the 'dir:' transport (default is manifest type of source)", Value: newOptionalStringValue(&opts.format), }, - cli.BoolFlag{ - Name: "dest-compress", - Usage: "Compress tarball image layers when saving to directory using the 'dir' transport. (default is same compression type as source)", - }, }, srcFlags...), destFlags...), } } diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index 26b16993..9a7ca3b0 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -74,7 +74,6 @@ func contextFromImageOptions(c *cli.Context, opts *imageOptions) (*types.SystemC OSChoice: opts.global.overrideOS, DockerCertPath: opts.dockerCertPath, OCISharedBlobDirPath: opts.sharedBlobDir, - DirForceCompress: c.Bool(opts.flagPrefix + "compress"), AuthFilePath: c.String("authfile"), DockerDaemonHost: opts.dockerDaemonHost, DockerDaemonCertPath: opts.dockerCertPath, @@ -102,7 +101,8 @@ func contextFromImageOptions(c *cli.Context, opts *imageOptions) (*types.SystemC // imageDestOptions is a superset of imageOptions specialized for iamge destinations. type imageDestOptions struct { *imageOptions - osTreeTmpDir string // A directory to use for OSTree temporary files + osTreeTmpDir string // A directory to use for OSTree temporary files + dirForceCompression bool // Compress layers when saving to the dir: transport } // imageDestFlags prepares a collection of CLI flags writing into imageDestOptions, and the managed imageDestOptions structure. @@ -116,6 +116,11 @@ func imageDestFlags(global *globalOptions, flagPrefix, credsOptionAlias string) Usage: "`DIRECTORY` to use for OSTree temporary files", Destination: &opts.osTreeTmpDir, }, + cli.BoolFlag{ + Name: flagPrefix + "compress", + Usage: "Compress tarball image layers when saving to directory using the 'dir' transport. (default is same compression type as source)", + Destination: &opts.dirForceCompression, + }, }...), &opts } @@ -128,6 +133,7 @@ func contextFromImageDestOptions(c *cli.Context, opts *imageDestOptions) (*types } ctx.OSTreeTmpDirPath = opts.osTreeTmpDir + ctx.DirForceCompress = opts.dirForceCompression return ctx, err } diff --git a/cmd/skopeo/utils_test.go b/cmd/skopeo/utils_test.go index 3934c465..615a77f8 100644 --- a/cmd/skopeo/utils_test.go +++ b/cmd/skopeo/utils_test.go @@ -71,14 +71,6 @@ func TestContextFromImageOptions(t *testing.T) { require.NoError(t, err) assert.Equal(t, &types.SystemContext{}, res) - // Explicitly set everything to default, except for when the default is “not present” - c, opts = fakeImageContext(t, "copy", "dest-", []string{}, []string{ - "--dest-compress=false", - }) - res, err = contextFromImageOptions(c, opts) - require.NoError(t, err) - assert.Equal(t, &types.SystemContext{}, res) - // Set everything to non-default values. c, opts = fakeImageContext(t, "copy", "dest-", []string{ "--registries.d", "/srv/registries.d", @@ -88,7 +80,6 @@ func TestContextFromImageOptions(t *testing.T) { "--authfile", "/srv/authfile", "--dest-cert-dir", "/srv/cert-dir", "--dest-shared-blob-dir", "/srv/shared-blob-dir", - "--dest-compress=true", "--dest-daemon-host", "daemon-host.example.com", "--dest-tls-verify=false", "--dest-creds", "creds-user:creds-password", @@ -107,7 +98,6 @@ func TestContextFromImageOptions(t *testing.T) { DockerDaemonCertPath: "/srv/cert-dir", DockerDaemonHost: "daemon-host.example.com", DockerDaemonInsecureSkipTLSVerify: true, - DirForceCompress: true, }, res) // Global/per-command tlsVerify behavior From 6ef45e5cf1e1eca503effb685402a3ff08260110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Wed, 18 Jul 2018 00:24:55 +0200 Subject: [PATCH 22/30] Migrate --authfile to sharedImageOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This introduces YET ANOTHER *Options structure, only to share this option between copy source and destination. (We do need to do this, because the libraries, rightly, refuse to work with source and destination declaring its own versino of the --authfile flag.) Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 13 +++++-------- cmd/skopeo/delete.go | 10 +++------- cmd/skopeo/inspect.go | 11 ++++------- cmd/skopeo/layers.go | 5 +++-- cmd/skopeo/utils.go | 42 +++++++++++++++++++++++++++++----------- cmd/skopeo/utils_test.go | 10 ++++++---- 6 files changed, 52 insertions(+), 39 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index f5b2e9c5..a1a71310 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -42,8 +42,9 @@ type copyOptions struct { } func copyCmd(global *globalOptions) cli.Command { - srcFlags, srcOpts := imageFlags(global, "src-", "screds") - destFlags, destOpts := imageDestFlags(global, "dest-", "dcreds") + sharedFlags, sharedOpts := sharedImageFlags() + srcFlags, srcOpts := imageFlags(global, sharedOpts, "src-", "screds") + destFlags, destOpts := imageDestFlags(global, sharedOpts, "dest-", "dcreds") opts := copyOptions{global: global, srcImage: srcOpts, destImage: destOpts, @@ -64,16 +65,12 @@ func copyCmd(global *globalOptions) cli.Command { ArgsUsage: "SOURCE-IMAGE DESTINATION-IMAGE", Action: opts.run, // FIXME: Do we need to namespace the GPG aspect? - Flags: append(append([]cli.Flag{ + Flags: append(append(append([]cli.Flag{ cli.StringSliceFlag{ Name: "additional-tag", Usage: "additional tags (supports docker-archive)", Value: &opts.additionalTags, // Surprisingly StringSliceFlag does not support Destination:, but modifies Value: in place. }, - cli.StringFlag{ - Name: "authfile", - Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", - }, cli.BoolFlag{ Name: "remove-signatures", Usage: "Do not copy signatures from SOURCE-IMAGE", @@ -89,7 +86,7 @@ func copyCmd(global *globalOptions) cli.Command { Usage: "`MANIFEST TYPE` (oci, v2s1, or v2s2) to use when saving image to directory using the 'dir:' transport (default is manifest type of source)", Value: newOptionalStringValue(&opts.format), }, - }, srcFlags...), destFlags...), + }, sharedFlags...), srcFlags...), destFlags...), } } diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go index a7d4f0d1..9a3723e8 100644 --- a/cmd/skopeo/delete.go +++ b/cmd/skopeo/delete.go @@ -16,7 +16,8 @@ type deleteOptions struct { } func deleteCmd(global *globalOptions) cli.Command { - imageFlags, imageOpts := imageFlags(global, "", "") + sharedFlags, sharedOpts := sharedImageFlags() + imageFlags, imageOpts := imageFlags(global, sharedOpts, "", "") opts := deleteOptions{ global: global, image: imageOpts, @@ -34,12 +35,7 @@ func deleteCmd(global *globalOptions) cli.Command { `, strings.Join(transports.ListNames(), ", ")), ArgsUsage: "IMAGE-NAME", Action: opts.run, - Flags: append([]cli.Flag{ - cli.StringFlag{ - Name: "authfile", - Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", - }, - }, imageFlags...), + Flags: append(sharedFlags, imageFlags...), } } diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 0bd7c928..0b01f077 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -36,7 +36,8 @@ type inspectOptions struct { } func inspectCmd(global *globalOptions) cli.Command { - imageFlags, imageOpts := imageFlags(global, "", "") + sharedFlags, sharedOpts := sharedImageFlags() + imageFlags, imageOpts := imageFlags(global, sharedOpts, "", "") opts := inspectOptions{ global: global, image: imageOpts, @@ -53,17 +54,13 @@ func inspectCmd(global *globalOptions) cli.Command { See skopeo(1) section "IMAGE NAMES" for the expected format `, strings.Join(transports.ListNames(), ", ")), ArgsUsage: "IMAGE-NAME", - Flags: append([]cli.Flag{ - cli.StringFlag{ - Name: "authfile", - Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", - }, + Flags: append(append([]cli.Flag{ cli.BoolFlag{ Name: "raw", Usage: "output raw manifest", Destination: &opts.raw, }, - }, imageFlags...), + }, sharedFlags...), imageFlags...), Action: opts.run, } } diff --git a/cmd/skopeo/layers.go b/cmd/skopeo/layers.go index e4c53518..8f5d08da 100644 --- a/cmd/skopeo/layers.go +++ b/cmd/skopeo/layers.go @@ -21,7 +21,8 @@ type layersOptions struct { } func layersCmd(global *globalOptions) cli.Command { - imageFlags, imageOpts := imageFlags(global, "", "") + sharedFlags, sharedOpts := sharedImageFlags() + imageFlags, imageOpts := imageFlags(global, sharedOpts, "", "") opts := layersOptions{ global: global, image: imageOpts, @@ -32,7 +33,7 @@ func layersCmd(global *globalOptions) cli.Command { ArgsUsage: "IMAGE-NAME [LAYER...]", Hidden: true, Action: opts.run, - Flags: imageFlags, + Flags: append(sharedFlags, imageFlags...), } } diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index 9a7ca3b0..d4c63841 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -10,22 +10,42 @@ import ( "github.com/urfave/cli" ) +// sharedImageOptions collects CLI flags which are image-related, but do not change across images. +// This really should be a part of globalOptions, but that would break existing users of (skopeo copy --authfile=). +type sharedImageOptions struct { + authFilePath string // Path to a */containers/auth.json +} + +// imageFlags prepares a collection of CLI flags writing into sharedImageOptions, and the managed sharedImageOptions structure. +func sharedImageFlags() ([]cli.Flag, *sharedImageOptions) { + opts := sharedImageOptions{} + return []cli.Flag{ + cli.StringFlag{ + Name: "authfile", + Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", + Destination: &opts.authFilePath, + }, + }, &opts +} + // imageOptions collects CLI flags which are the same across subcommands, but may be different for each image // (e.g. may differ between the source and destination of a copy) type imageOptions struct { - global *globalOptions // May be shared across several imageOptions instances. - flagPrefix string // FIXME: Drop this eventually. - credsOption optionalString // username[:password] for accessing a registry - dockerCertPath string // A directory using Docker-like *.{crt,cert,key} files for connecting to a registry or a daemon - tlsVerify optionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:) - sharedBlobDir string // A directory to use for OCI blobs, shared across repositories - dockerDaemonHost string // docker-daemon: host to connect to + global *globalOptions // May be shared across several imageOptions instances. + shared *sharedImageOptions // May be shared across several imageOptions instances. + flagPrefix string // FIXME: Drop this eventually. + credsOption optionalString // username[:password] for accessing a registry + dockerCertPath string // A directory using Docker-like *.{crt,cert,key} files for connecting to a registry or a daemon + tlsVerify optionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:) + sharedBlobDir string // A directory to use for OCI blobs, shared across repositories + dockerDaemonHost string // docker-daemon: host to connect to } // imageFlags prepares a collection of CLI flags writing into imageOptions, and the managed imageOptions structure. -func imageFlags(global *globalOptions, flagPrefix, credsOptionAlias string) ([]cli.Flag, *imageOptions) { +func imageFlags(global *globalOptions, shared *sharedImageOptions, flagPrefix, credsOptionAlias string) ([]cli.Flag, *imageOptions) { opts := imageOptions{ global: global, + shared: shared, flagPrefix: flagPrefix, } @@ -74,7 +94,7 @@ func contextFromImageOptions(c *cli.Context, opts *imageOptions) (*types.SystemC OSChoice: opts.global.overrideOS, DockerCertPath: opts.dockerCertPath, OCISharedBlobDirPath: opts.sharedBlobDir, - AuthFilePath: c.String("authfile"), + AuthFilePath: opts.shared.authFilePath, DockerDaemonHost: opts.dockerDaemonHost, DockerDaemonCertPath: opts.dockerCertPath, } @@ -106,8 +126,8 @@ type imageDestOptions struct { } // imageDestFlags prepares a collection of CLI flags writing into imageDestOptions, and the managed imageDestOptions structure. -func imageDestFlags(global *globalOptions, flagPrefix, credsOptionAlias string) ([]cli.Flag, *imageDestOptions) { - genericFlags, genericOptions := imageFlags(global, flagPrefix, credsOptionAlias) +func imageDestFlags(global *globalOptions, shared *sharedImageOptions, flagPrefix, credsOptionAlias string) ([]cli.Flag, *imageDestOptions) { + genericFlags, genericOptions := imageFlags(global, shared, flagPrefix, credsOptionAlias) opts := imageDestOptions{imageOptions: genericOptions} return append(genericFlags, []cli.Flag{ diff --git a/cmd/skopeo/utils_test.go b/cmd/skopeo/utils_test.go index 615a77f8..8dfdaa06 100644 --- a/cmd/skopeo/utils_test.go +++ b/cmd/skopeo/utils_test.go @@ -35,7 +35,8 @@ func fakeImageContext(t *testing.T, cmdName string, flagPrefix string, globalFla cmd := app.Command(cmdName) require.NotNil(t, cmd) - imageFlags, imageOpts := imageFlags(globalOpts, flagPrefix, "") + sharedFlags, sharedOpts := sharedImageFlags() + imageFlags, imageOpts := imageFlags(globalOpts, sharedOpts, flagPrefix, "") appliedFlags := map[string]struct{}{} // Ugly: cmd.Flags includes imageFlags as well. For now, we need cmd.Flags to apply here // to be able to test the non-Destination: flags, but we must not apply the same flag name twice. @@ -46,7 +47,7 @@ func fakeImageContext(t *testing.T, cmdName string, flagPrefix string, globalFla return strings.Split(f.GetName(), ",")[0] } flagSet := flag.NewFlagSet(cmd.Name, flag.ContinueOnError) - for _, f := range imageFlags { + for _, f := range append(sharedFlags, imageFlags...) { f.Apply(flagSet) appliedFlags[firstName(f)] = struct{}{} } @@ -145,7 +146,8 @@ func fakeImageDestContext(t *testing.T, cmdName string, flagPrefix string, globa cmd := app.Command(cmdName) require.NotNil(t, cmd) - imageFlags, imageOpts := imageDestFlags(globalOpts, flagPrefix, "") + sharedFlags, sharedOpts := sharedImageFlags() + imageFlags, imageOpts := imageDestFlags(globalOpts, sharedOpts, flagPrefix, "") appliedFlags := map[string]struct{}{} // Ugly: cmd.Flags includes imageFlags as well. For now, we need cmd.Flags to apply here // to be able to test the non-Destination: flags, but we must not apply the same flag name twice. @@ -156,7 +158,7 @@ func fakeImageDestContext(t *testing.T, cmdName string, flagPrefix string, globa return strings.Split(f.GetName(), ",")[0] } flagSet := flag.NewFlagSet(cmd.Name, flag.ContinueOnError) - for _, f := range imageFlags { + for _, f := range append(sharedFlags, imageFlags...) { f.Apply(flagSet) appliedFlags[firstName(f)] = struct{}{} } From 8084f6f4e2bcceec9a9aa704f78f07fed85d869e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Wed, 18 Jul 2018 00:55:09 +0200 Subject: [PATCH 23/30] No longer define all "skopeo copy" flags in utils_test.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All the contextFromImage{,Dest}Options flags are now defined in imageFlags/imageDestFlags. Signed-off-by: Miloslav Trmač --- cmd/skopeo/utils_test.go | 69 +++++++--------------------------------- 1 file changed, 12 insertions(+), 57 deletions(-) diff --git a/cmd/skopeo/utils_test.go b/cmd/skopeo/utils_test.go index 8dfdaa06..b51c4cae 100644 --- a/cmd/skopeo/utils_test.go +++ b/cmd/skopeo/utils_test.go @@ -2,7 +2,6 @@ package main import ( "flag" - "strings" "testing" "github.com/containers/image/types" @@ -29,51 +28,29 @@ func fakeGlobalOptions(t *testing.T, flags []string) (*cli.App, *cli.Context, *g // fakeImageContext creates inputs for contextFromImageOptions. // NOTE: This is QUITE FAKE; none of the urfave/cli normalization and the like happens. -func fakeImageContext(t *testing.T, cmdName string, flagPrefix string, globalFlags []string, cmdFlags []string) (*cli.Context, *imageOptions) { +func fakeImageContext(t *testing.T, flagPrefix string, globalFlags []string, cmdFlags []string) (*cli.Context, *imageOptions) { app, globalCtx, globalOpts := fakeGlobalOptions(t, globalFlags) - cmd := app.Command(cmdName) - require.NotNil(t, cmd) - sharedFlags, sharedOpts := sharedImageFlags() imageFlags, imageOpts := imageFlags(globalOpts, sharedOpts, flagPrefix, "") - appliedFlags := map[string]struct{}{} - // Ugly: cmd.Flags includes imageFlags as well. For now, we need cmd.Flags to apply here - // to be able to test the non-Destination: flags, but we must not apply the same flag name twice. - // So, primarily use imageFlags (so that Destination: is used as expected), and then follow up with - // the remaining flags from cmd.Flags (so that cli.Context.String() etc. works). - // This is horribly ugly, but all of this will disappear within this PR. - firstName := func(f cli.Flag) string { // We even need to recognize "dest-creds,dcreds". This will disappear as well. - return strings.Split(f.GetName(), ",")[0] - } - flagSet := flag.NewFlagSet(cmd.Name, flag.ContinueOnError) + flagSet := flag.NewFlagSet("fakeImageContext", flag.ContinueOnError) for _, f := range append(sharedFlags, imageFlags...) { f.Apply(flagSet) - appliedFlags[firstName(f)] = struct{}{} } - for _, f := range cmd.Flags { - if _, ok := appliedFlags[firstName(f)]; !ok { - f.Apply(flagSet) - appliedFlags[firstName(f)] = struct{}{} - } - } - err := flagSet.Parse(cmdFlags) require.NoError(t, err) return cli.NewContext(app, flagSet, globalCtx), imageOpts } func TestContextFromImageOptions(t *testing.T) { - // FIXME: All of this only tests (skopeo copy --dest) - // Default state - c, opts := fakeImageContext(t, "copy", "dest-", []string{}, []string{}) + c, opts := fakeImageContext(t, "dest-", []string{}, []string{}) res, err := contextFromImageOptions(c, opts) require.NoError(t, err) assert.Equal(t, &types.SystemContext{}, res) // Set everything to non-default values. - c, opts = fakeImageContext(t, "copy", "dest-", []string{ + c, opts = fakeImageContext(t, "dest-", []string{ "--registries.d", "/srv/registries.d", "--override-arch", "overridden-arch", "--override-os", "overridden-os", @@ -125,7 +102,7 @@ func TestContextFromImageOptions(t *testing.T) { if c.cmd != "" { cmdFlags = append(cmdFlags, "--dest-tls-verify="+c.cmd) } - ctx, opts := fakeImageContext(t, "copy", "dest-", globalFlags, cmdFlags) + ctx, opts := fakeImageContext(t, "dest-", globalFlags, cmdFlags) res, err = contextFromImageOptions(ctx, opts) require.NoError(t, err) assert.Equal(t, c.expectedDocker, res.DockerInsecureSkipTLSVerify, "%#v", c) @@ -133,58 +110,36 @@ func TestContextFromImageOptions(t *testing.T) { } // Invalid option values - c, opts = fakeImageContext(t, "copy", "dest-", []string{}, []string{"--dest-creds", ""}) + c, opts = fakeImageContext(t, "dest-", []string{}, []string{"--dest-creds", ""}) _, err = contextFromImageOptions(c, opts) assert.Error(t, err) } // fakeImageDestContext creates inputs for contextFromImageDestOptions. // NOTE: This is QUITE FAKE; none of the urfave/cli normalization and the like happens. -func fakeImageDestContext(t *testing.T, cmdName string, flagPrefix string, globalFlags []string, cmdFlags []string) (*cli.Context, *imageDestOptions) { +func fakeImageDestContext(t *testing.T, flagPrefix string, globalFlags []string, cmdFlags []string) (*cli.Context, *imageDestOptions) { app, globalCtx, globalOpts := fakeGlobalOptions(t, globalFlags) - cmd := app.Command(cmdName) - require.NotNil(t, cmd) - sharedFlags, sharedOpts := sharedImageFlags() imageFlags, imageOpts := imageDestFlags(globalOpts, sharedOpts, flagPrefix, "") - appliedFlags := map[string]struct{}{} - // Ugly: cmd.Flags includes imageFlags as well. For now, we need cmd.Flags to apply here - // to be able to test the non-Destination: flags, but we must not apply the same flag name twice. - // So, primarily use imageFlags (so that Destination: is used as expected), and then follow up with - // the remaining flags from cmd.Flags (so that cli.Context.String() etc. works). - // This is horribly ugly, but all of this will disappear within this PR. - firstName := func(f cli.Flag) string { // We even need to recognize "dest-creds,dcreds". This will disappear as well. - return strings.Split(f.GetName(), ",")[0] - } - flagSet := flag.NewFlagSet(cmd.Name, flag.ContinueOnError) + flagSet := flag.NewFlagSet("fakeImageDestContext", flag.ContinueOnError) for _, f := range append(sharedFlags, imageFlags...) { f.Apply(flagSet) - appliedFlags[firstName(f)] = struct{}{} } - for _, f := range cmd.Flags { - if _, ok := appliedFlags[firstName(f)]; !ok { - f.Apply(flagSet) - appliedFlags[firstName(f)] = struct{}{} - } - } - err := flagSet.Parse(cmdFlags) require.NoError(t, err) return cli.NewContext(app, flagSet, globalCtx), imageOpts } func TestContextFromImageDestOptions(t *testing.T) { - // FIXME: All of this only tests (skopeo copy --dest) - // Default state - c, opts := fakeImageDestContext(t, "copy", "dest-", []string{}, []string{}) + c, opts := fakeImageDestContext(t, "dest-", []string{}, []string{}) res, err := contextFromImageDestOptions(c, opts) require.NoError(t, err) assert.Equal(t, &types.SystemContext{}, res) // Explicitly set everything to default, except for when the default is “not present” - c, opts = fakeImageDestContext(t, "copy", "dest-", []string{}, []string{ + c, opts = fakeImageDestContext(t, "dest-", []string{}, []string{ "--dest-compress=false", }) res, err = contextFromImageDestOptions(c, opts) @@ -192,7 +147,7 @@ func TestContextFromImageDestOptions(t *testing.T) { assert.Equal(t, &types.SystemContext{}, res) // Set everything to non-default values. - c, opts = fakeImageDestContext(t, "copy", "dest-", []string{ + c, opts = fakeImageDestContext(t, "dest-", []string{ "--registries.d", "/srv/registries.d", "--override-arch", "overridden-arch", "--override-os", "overridden-os", @@ -225,7 +180,7 @@ func TestContextFromImageDestOptions(t *testing.T) { }, res) // Invalid option values in imageOptions - c, opts = fakeImageDestContext(t, "copy", "dest-", []string{}, []string{"--dest-creds", ""}) + c, opts = fakeImageDestContext(t, "dest-", []string{}, []string{"--dest-creds", ""}) _, err = contextFromImageDestOptions(c, opts) assert.Error(t, err) } From 39de98777d6adf3487e3c13236e8c8b795f50a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Wed, 11 Jul 2018 22:38:01 +0200 Subject: [PATCH 24/30] Remove no longer needed flagsPrefix from imageOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit contextFromImageOptions is finally not using any string-based lookup in cli.Context, so we don't need to record this value any more. Signed-off-by: Miloslav Trmač --- cmd/skopeo/utils.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index d4c63841..bf9a3108 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -33,7 +33,6 @@ func sharedImageFlags() ([]cli.Flag, *sharedImageOptions) { type imageOptions struct { global *globalOptions // May be shared across several imageOptions instances. shared *sharedImageOptions // May be shared across several imageOptions instances. - flagPrefix string // FIXME: Drop this eventually. credsOption optionalString // username[:password] for accessing a registry dockerCertPath string // A directory using Docker-like *.{crt,cert,key} files for connecting to a registry or a daemon tlsVerify optionalBool // Require HTTPS and verify certificates (for docker: and docker-daemon:) @@ -44,9 +43,8 @@ type imageOptions struct { // imageFlags prepares a collection of CLI flags writing into imageOptions, and the managed imageOptions structure. func imageFlags(global *globalOptions, shared *sharedImageOptions, flagPrefix, credsOptionAlias string) ([]cli.Flag, *imageOptions) { opts := imageOptions{ - global: global, - shared: shared, - flagPrefix: flagPrefix, + global: global, + shared: shared, } // This is horribly ugly, but we need to support the old option forms of (skopeo copy) for compatibility. From 3094320203e24f2d79f8e3aa728d22d387fbff40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Wed, 18 Jul 2018 00:56:32 +0200 Subject: [PATCH 25/30] Replace contextFromImageOptions by imageOptions.newSystemContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We no longer need the *cli.Context parameter, and at that point it looks much cleaner to make this a method (already individually; it will be even cleaner after a similar imageDestOptions conversion). Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 2 +- cmd/skopeo/delete.go | 2 +- cmd/skopeo/inspect.go | 2 +- cmd/skopeo/layers.go | 2 +- cmd/skopeo/utils.go | 10 +++++----- cmd/skopeo/utils_test.go | 28 ++++++++++++++-------------- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index a1a71310..47795457 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -18,7 +18,7 @@ import ( // contextsFromCopyOptions returns source and destionation types.SystemContext depending on c. func contextsFromCopyOptions(c *cli.Context, opts *copyOptions) (*types.SystemContext, *types.SystemContext, error) { - sourceCtx, err := contextFromImageOptions(c, opts.srcImage) + sourceCtx, err := opts.srcImage.newSystemContext() if err != nil { return nil, nil, err } diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go index 9a3723e8..72b85fcf 100644 --- a/cmd/skopeo/delete.go +++ b/cmd/skopeo/delete.go @@ -49,7 +49,7 @@ func (opts *deleteOptions) run(c *cli.Context) error { return fmt.Errorf("Invalid source name %s: %v", c.Args()[0], err) } - sys, err := contextFromImageOptions(c, opts.image) + sys, err := opts.image.newSystemContext() if err != nil { return err } diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 0b01f077..1bea45f9 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -115,7 +115,7 @@ func (opts *inspectOptions) run(c *cli.Context) (retErr error) { outputData.Name = dockerRef.Name() } if img.Reference().Transport() == docker.Transport { - sys, err := contextFromImageOptions(c, opts.image) + sys, err := opts.image.newSystemContext() if err != nil { return err } diff --git a/cmd/skopeo/layers.go b/cmd/skopeo/layers.go index 8f5d08da..271c9f49 100644 --- a/cmd/skopeo/layers.go +++ b/cmd/skopeo/layers.go @@ -46,7 +46,7 @@ func (opts *layersOptions) run(c *cli.Context) (retErr error) { ctx, cancel := opts.global.commandTimeoutContext() defer cancel() - sys, err := contextFromImageOptions(c, opts.image) + sys, err := opts.image.newSystemContext() if err != nil { return err } diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index bf9a3108..a2a6999c 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -83,9 +83,9 @@ func imageFlags(global *globalOptions, shared *sharedImageOptions, flagPrefix, c }, &opts } -// contextFromImageOptions returns a *types.SystemContext corresponding to opts. +// newSystemContext returns a *types.SystemContext corresponding to opts. // It is guaranteed to return a fresh instance, so it is safe to make additional updates to it. -func contextFromImageOptions(c *cli.Context, opts *imageOptions) (*types.SystemContext, error) { +func (opts *imageOptions) newSystemContext() (*types.SystemContext, error) { ctx := &types.SystemContext{ RegistriesDirPath: opts.global.registriesDirPath, ArchitectureChoice: opts.global.overrideArch, @@ -145,7 +145,7 @@ func imageDestFlags(global *globalOptions, shared *sharedImageOptions, flagPrefi // contextFromImageDestOptions returns a *types.SystemContext corresponding to opts. // It is guaranteed to return a fresh instance, so it is safe to make additional updates to it. func contextFromImageDestOptions(c *cli.Context, opts *imageDestOptions) (*types.SystemContext, error) { - ctx, err := contextFromImageOptions(c, opts.imageOptions) + ctx, err := opts.imageOptions.newSystemContext() if err != nil { return nil, err } @@ -188,7 +188,7 @@ func parseImage(ctx context.Context, c *cli.Context, opts *imageOptions) (types. if err != nil { return nil, err } - sys, err := contextFromImageOptions(c, opts) + sys, err := opts.newSystemContext() if err != nil { return nil, err } @@ -202,7 +202,7 @@ func parseImageSource(ctx context.Context, c *cli.Context, opts *imageOptions, n if err != nil { return nil, err } - sys, err := contextFromImageOptions(c, opts) + sys, err := opts.newSystemContext() if err != nil { return nil, err } diff --git a/cmd/skopeo/utils_test.go b/cmd/skopeo/utils_test.go index b51c4cae..8c1543ca 100644 --- a/cmd/skopeo/utils_test.go +++ b/cmd/skopeo/utils_test.go @@ -26,31 +26,31 @@ func fakeGlobalOptions(t *testing.T, flags []string) (*cli.App, *cli.Context, *g return app, ctx, opts } -// fakeImageContext creates inputs for contextFromImageOptions. +// fakeImageOptions creates imageOptions and sets it according to globalFlags/cmdFlags. // NOTE: This is QUITE FAKE; none of the urfave/cli normalization and the like happens. -func fakeImageContext(t *testing.T, flagPrefix string, globalFlags []string, cmdFlags []string) (*cli.Context, *imageOptions) { - app, globalCtx, globalOpts := fakeGlobalOptions(t, globalFlags) +func fakeImageOptions(t *testing.T, flagPrefix string, globalFlags []string, cmdFlags []string) *imageOptions { + _, _, globalOpts := fakeGlobalOptions(t, globalFlags) sharedFlags, sharedOpts := sharedImageFlags() imageFlags, imageOpts := imageFlags(globalOpts, sharedOpts, flagPrefix, "") - flagSet := flag.NewFlagSet("fakeImageContext", flag.ContinueOnError) + flagSet := flag.NewFlagSet("fakeImageOptions", flag.ContinueOnError) for _, f := range append(sharedFlags, imageFlags...) { f.Apply(flagSet) } err := flagSet.Parse(cmdFlags) require.NoError(t, err) - return cli.NewContext(app, flagSet, globalCtx), imageOpts + return imageOpts } -func TestContextFromImageOptions(t *testing.T) { +func TestImageOptionsNewSystemContext(t *testing.T) { // Default state - c, opts := fakeImageContext(t, "dest-", []string{}, []string{}) - res, err := contextFromImageOptions(c, opts) + opts := fakeImageOptions(t, "dest-", []string{}, []string{}) + res, err := opts.newSystemContext() require.NoError(t, err) assert.Equal(t, &types.SystemContext{}, res) // Set everything to non-default values. - c, opts = fakeImageContext(t, "dest-", []string{ + opts = fakeImageOptions(t, "dest-", []string{ "--registries.d", "/srv/registries.d", "--override-arch", "overridden-arch", "--override-os", "overridden-os", @@ -62,7 +62,7 @@ func TestContextFromImageOptions(t *testing.T) { "--dest-tls-verify=false", "--dest-creds", "creds-user:creds-password", }) - res, err = contextFromImageOptions(c, opts) + res, err = opts.newSystemContext() require.NoError(t, err) assert.Equal(t, &types.SystemContext{ RegistriesDirPath: "/srv/registries.d", @@ -102,16 +102,16 @@ func TestContextFromImageOptions(t *testing.T) { if c.cmd != "" { cmdFlags = append(cmdFlags, "--dest-tls-verify="+c.cmd) } - ctx, opts := fakeImageContext(t, "dest-", globalFlags, cmdFlags) - res, err = contextFromImageOptions(ctx, opts) + opts := fakeImageOptions(t, "dest-", globalFlags, cmdFlags) + res, err = opts.newSystemContext() require.NoError(t, err) assert.Equal(t, c.expectedDocker, res.DockerInsecureSkipTLSVerify, "%#v", c) assert.Equal(t, c.expectedDockerDaemon, res.DockerDaemonInsecureSkipTLSVerify, "%#v", c) } // Invalid option values - c, opts = fakeImageContext(t, "dest-", []string{}, []string{"--dest-creds", ""}) - _, err = contextFromImageOptions(c, opts) + opts = fakeImageOptions(t, "dest-", []string{}, []string{"--dest-creds", ""}) + _, err = opts.newSystemContext() assert.Error(t, err) } From 1d1bf0d3936c5284de6fe49800b1c6b55d7cc7ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Wed, 18 Jul 2018 00:58:20 +0200 Subject: [PATCH 26/30] Replace contextFromImageDestOptions by imageDestOptions.newSystemContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is analogous to the imageOptions.newSystemContext conversion. Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 8 ++++---- cmd/skopeo/utils.go | 4 ++-- cmd/skopeo/utils_test.go | 36 +++++++++++++++++------------------- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 47795457..75aa6226 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -16,14 +16,14 @@ import ( "github.com/urfave/cli" ) -// contextsFromCopyOptions returns source and destionation types.SystemContext depending on c. -func contextsFromCopyOptions(c *cli.Context, opts *copyOptions) (*types.SystemContext, *types.SystemContext, error) { +// contextsFromCopyOptions returns source and destionation types.SystemContext depending on opts. +func contextsFromCopyOptions(opts *copyOptions) (*types.SystemContext, *types.SystemContext, error) { sourceCtx, err := opts.srcImage.newSystemContext() if err != nil { return nil, nil, err } - destinationCtx, err := contextFromImageDestOptions(c, opts.destImage) + destinationCtx, err := opts.destImage.newSystemContext() if err != nil { return nil, nil, err } @@ -111,7 +111,7 @@ func (opts *copyOptions) run(c *cli.Context) error { return fmt.Errorf("Invalid destination name %s: %v", c.Args()[1], err) } - sourceCtx, destinationCtx, err := contextsFromCopyOptions(c, opts) + sourceCtx, destinationCtx, err := contextsFromCopyOptions(opts) if err != nil { return err } diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index a2a6999c..ac6cfa81 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -142,9 +142,9 @@ func imageDestFlags(global *globalOptions, shared *sharedImageOptions, flagPrefi }...), &opts } -// contextFromImageDestOptions returns a *types.SystemContext corresponding to opts. +// newSystemContext returns a *types.SystemContext corresponding to opts. // It is guaranteed to return a fresh instance, so it is safe to make additional updates to it. -func contextFromImageDestOptions(c *cli.Context, opts *imageDestOptions) (*types.SystemContext, error) { +func (opts *imageDestOptions) newSystemContext() (*types.SystemContext, error) { ctx, err := opts.imageOptions.newSystemContext() if err != nil { return nil, err diff --git a/cmd/skopeo/utils_test.go b/cmd/skopeo/utils_test.go index 8c1543ca..f88653df 100644 --- a/cmd/skopeo/utils_test.go +++ b/cmd/skopeo/utils_test.go @@ -7,12 +7,11 @@ import ( "github.com/containers/image/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/urfave/cli" ) // fakeGlobalOptions creates globalOptions and sets it according to flags. // NOTE: This is QUITE FAKE; none of the urfave/cli normalization and the like happens. -func fakeGlobalOptions(t *testing.T, flags []string) (*cli.App, *cli.Context, *globalOptions) { +func fakeGlobalOptions(t *testing.T, flags []string) *globalOptions { app, opts := createApp() flagSet := flag.NewFlagSet(app.Name, flag.ContinueOnError) @@ -21,15 +20,14 @@ func fakeGlobalOptions(t *testing.T, flags []string) (*cli.App, *cli.Context, *g } err := flagSet.Parse(flags) require.NoError(t, err) - ctx := cli.NewContext(app, flagSet, nil) - return app, ctx, opts + return opts } // fakeImageOptions creates imageOptions and sets it according to globalFlags/cmdFlags. // NOTE: This is QUITE FAKE; none of the urfave/cli normalization and the like happens. func fakeImageOptions(t *testing.T, flagPrefix string, globalFlags []string, cmdFlags []string) *imageOptions { - _, _, globalOpts := fakeGlobalOptions(t, globalFlags) + globalOpts := fakeGlobalOptions(t, globalFlags) sharedFlags, sharedOpts := sharedImageFlags() imageFlags, imageOpts := imageFlags(globalOpts, sharedOpts, flagPrefix, "") @@ -115,39 +113,39 @@ func TestImageOptionsNewSystemContext(t *testing.T) { assert.Error(t, err) } -// fakeImageDestContext creates inputs for contextFromImageDestOptions. +// fakeImageDestOptions creates imageDestOptions and sets it according to globalFlags/cmdFlags. // NOTE: This is QUITE FAKE; none of the urfave/cli normalization and the like happens. -func fakeImageDestContext(t *testing.T, flagPrefix string, globalFlags []string, cmdFlags []string) (*cli.Context, *imageDestOptions) { - app, globalCtx, globalOpts := fakeGlobalOptions(t, globalFlags) +func fakeImageDestOptions(t *testing.T, flagPrefix string, globalFlags []string, cmdFlags []string) *imageDestOptions { + globalOpts := fakeGlobalOptions(t, globalFlags) sharedFlags, sharedOpts := sharedImageFlags() imageFlags, imageOpts := imageDestFlags(globalOpts, sharedOpts, flagPrefix, "") - flagSet := flag.NewFlagSet("fakeImageDestContext", flag.ContinueOnError) + flagSet := flag.NewFlagSet("fakeImageDestOptions", flag.ContinueOnError) for _, f := range append(sharedFlags, imageFlags...) { f.Apply(flagSet) } err := flagSet.Parse(cmdFlags) require.NoError(t, err) - return cli.NewContext(app, flagSet, globalCtx), imageOpts + return imageOpts } -func TestContextFromImageDestOptions(t *testing.T) { +func TestImageDestOptionsNewSystemContext(t *testing.T) { // Default state - c, opts := fakeImageDestContext(t, "dest-", []string{}, []string{}) - res, err := contextFromImageDestOptions(c, opts) + opts := fakeImageDestOptions(t, "dest-", []string{}, []string{}) + res, err := opts.newSystemContext() require.NoError(t, err) assert.Equal(t, &types.SystemContext{}, res) // Explicitly set everything to default, except for when the default is “not present” - c, opts = fakeImageDestContext(t, "dest-", []string{}, []string{ + opts = fakeImageDestOptions(t, "dest-", []string{}, []string{ "--dest-compress=false", }) - res, err = contextFromImageDestOptions(c, opts) + res, err = opts.newSystemContext() require.NoError(t, err) assert.Equal(t, &types.SystemContext{}, res) // Set everything to non-default values. - c, opts = fakeImageDestContext(t, "dest-", []string{ + opts = fakeImageDestOptions(t, "dest-", []string{ "--registries.d", "/srv/registries.d", "--override-arch", "overridden-arch", "--override-os", "overridden-os", @@ -161,7 +159,7 @@ func TestContextFromImageDestOptions(t *testing.T) { "--dest-tls-verify=false", "--dest-creds", "creds-user:creds-password", }) - res, err = contextFromImageDestOptions(c, opts) + res, err = opts.newSystemContext() require.NoError(t, err) assert.Equal(t, &types.SystemContext{ RegistriesDirPath: "/srv/registries.d", @@ -180,7 +178,7 @@ func TestContextFromImageDestOptions(t *testing.T) { }, res) // Invalid option values in imageOptions - c, opts = fakeImageDestContext(t, "dest-", []string{}, []string{"--dest-creds", ""}) - _, err = contextFromImageDestOptions(c, opts) + opts = fakeImageDestOptions(t, "dest-", []string{}, []string{"--dest-creds", ""}) + _, err = opts.newSystemContext() assert.Error(t, err) } From 958cafb2c0241630bcc0e162114433c49f631709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Wed, 11 Jul 2018 22:58:32 +0200 Subject: [PATCH 27/30] Inline contextsFromCopyOptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was not really any clearer when broken out. We already have a pair of trivial src/dest API calls before this, so adding a similar src/dest call for SystemContext follows the pattern. Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 75aa6226..442af8d6 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -11,26 +11,10 @@ import ( "github.com/containers/image/manifest" "github.com/containers/image/transports" "github.com/containers/image/transports/alltransports" - "github.com/containers/image/types" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/urfave/cli" ) -// contextsFromCopyOptions returns source and destionation types.SystemContext depending on opts. -func contextsFromCopyOptions(opts *copyOptions) (*types.SystemContext, *types.SystemContext, error) { - sourceCtx, err := opts.srcImage.newSystemContext() - if err != nil { - return nil, nil, err - } - - destinationCtx, err := opts.destImage.newSystemContext() - if err != nil { - return nil, nil, err - } - - return sourceCtx, destinationCtx, nil -} - type copyOptions struct { global *globalOptions srcImage *imageOptions @@ -111,7 +95,11 @@ func (opts *copyOptions) run(c *cli.Context) error { return fmt.Errorf("Invalid destination name %s: %v", c.Args()[1], err) } - sourceCtx, destinationCtx, err := contextsFromCopyOptions(opts) + sourceCtx, err := opts.srcImage.newSystemContext() + if err != nil { + return err + } + destinationCtx, err := opts.destImage.newSystemContext() if err != nil { return err } From afa92d58f6bf118598950f9435069e0732f86206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Wed, 11 Jul 2018 23:11:49 +0200 Subject: [PATCH 28/30] Drop the *cli.Context argument from parseImage and parseImageSource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We no longer need it for handling flags. Also, require the caller to explicitly pass an image name to parseImage instead of, horribly nontransparently, using the first CLI option. Signed-off-by: Miloslav Trmač --- cmd/skopeo/inspect.go | 2 +- cmd/skopeo/layers.go | 2 +- cmd/skopeo/utils.go | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 1bea45f9..9cd7b3dd 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -69,7 +69,7 @@ func (opts *inspectOptions) run(c *cli.Context) (retErr error) { ctx, cancel := opts.global.commandTimeoutContext() defer cancel() - img, err := parseImage(ctx, c, opts.image) + img, err := parseImage(ctx, opts.image, c.Args().First()) if err != nil { return err } diff --git a/cmd/skopeo/layers.go b/cmd/skopeo/layers.go index 271c9f49..aa8b296a 100644 --- a/cmd/skopeo/layers.go +++ b/cmd/skopeo/layers.go @@ -51,7 +51,7 @@ func (opts *layersOptions) run(c *cli.Context) (retErr error) { return err } cache := blobinfocache.DefaultCache(sys) - rawSource, err := parseImageSource(ctx, c, opts.image, c.Args()[0]) + rawSource, err := parseImageSource(ctx, opts.image, c.Args()[0]) if err != nil { return err } diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index ac6cfa81..66e0e6f0 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -182,9 +182,8 @@ func getDockerAuth(creds string) (*types.DockerAuthConfig, error) { // parseImage converts image URL-like string to an initialized handler for that image. // The caller must call .Close() on the returned ImageCloser. -func parseImage(ctx context.Context, c *cli.Context, opts *imageOptions) (types.ImageCloser, error) { - imgName := c.Args().First() - ref, err := alltransports.ParseImageName(imgName) +func parseImage(ctx context.Context, opts *imageOptions, name string) (types.ImageCloser, error) { + ref, err := alltransports.ParseImageName(name) if err != nil { return nil, err } @@ -197,7 +196,7 @@ func parseImage(ctx context.Context, c *cli.Context, opts *imageOptions) (types. // parseImageSource converts image URL-like string to an ImageSource. // The caller must call .Close() on the returned ImageSource. -func parseImageSource(ctx context.Context, c *cli.Context, opts *imageOptions, name string) (types.ImageSource, error) { +func parseImageSource(ctx context.Context, opts *imageOptions, name string) (types.ImageSource, error) { ref, err := alltransports.ParseImageName(name) if err != nil { return nil, err From 2497f500d5fce80a48037f11b3c86b0bad0dab10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Wed, 18 Jul 2018 00:41:39 +0200 Subject: [PATCH 29/30] Add commandAction to make *cli.Context unavailable in command handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That in turn makes sure that the cli.String() etc. flag access functions are not used, and all flag handling is done using the *Options structures and the Destination: members of cli.Flag. Signed-off-by: Miloslav Trmač --- cmd/skopeo/copy.go | 21 ++++++++++----------- cmd/skopeo/delete.go | 11 ++++++----- cmd/skopeo/inspect.go | 14 +++++++++----- cmd/skopeo/layers.go | 11 ++++++----- cmd/skopeo/manifest.go | 11 ++++++----- cmd/skopeo/signing.go | 39 ++++++++++++++++++++------------------- cmd/skopeo/utils.go | 21 +++++++++++++++++++++ 7 files changed, 78 insertions(+), 50 deletions(-) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index 442af8d6..dd285600 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -3,7 +3,7 @@ package main import ( "errors" "fmt" - "os" + "io" "strings" "github.com/containers/image/copy" @@ -47,7 +47,7 @@ func copyCmd(global *globalOptions) cli.Command { See skopeo(1) section "IMAGE NAMES" for the expected format `, strings.Join(transports.ListNames(), ", ")), ArgsUsage: "SOURCE-IMAGE DESTINATION-IMAGE", - Action: opts.run, + Action: commandAction(opts.run), // FIXME: Do we need to namespace the GPG aspect? Flags: append(append(append([]cli.Flag{ cli.StringSliceFlag{ @@ -74,10 +74,9 @@ func copyCmd(global *globalOptions) cli.Command { } } -func (opts *copyOptions) run(c *cli.Context) error { - if len(c.Args()) != 2 { - cli.ShowCommandHelp(c, "copy") - return errors.New("Exactly two arguments expected") +func (opts *copyOptions) run(args []string, stdout io.Writer) error { + if len(args) != 2 { + return errorShouldDisplayUsage{errors.New("Exactly two arguments expected")} } policyContext, err := opts.global.getPolicyContext() @@ -86,13 +85,13 @@ func (opts *copyOptions) run(c *cli.Context) error { } defer policyContext.Destroy() - srcRef, err := alltransports.ParseImageName(c.Args()[0]) + srcRef, err := alltransports.ParseImageName(args[0]) if err != nil { - return fmt.Errorf("Invalid source name %s: %v", c.Args()[0], err) + return fmt.Errorf("Invalid source name %s: %v", args[0], err) } - destRef, err := alltransports.ParseImageName(c.Args()[1]) + destRef, err := alltransports.ParseImageName(args[1]) if err != nil { - return fmt.Errorf("Invalid destination name %s: %v", c.Args()[1], err) + return fmt.Errorf("Invalid destination name %s: %v", args[1], err) } sourceCtx, err := opts.srcImage.newSystemContext() @@ -136,7 +135,7 @@ func (opts *copyOptions) run(c *cli.Context) error { _, err = copy.Image(ctx, policyContext, destRef, srcRef, ©.Options{ RemoveSignatures: opts.removeSignatures, SignBy: opts.signByFingerprint, - ReportWriter: os.Stdout, + ReportWriter: stdout, SourceCtx: sourceCtx, DestinationCtx: destinationCtx, ForceManifestMIMEType: manifestType, diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go index 72b85fcf..07a7cea2 100644 --- a/cmd/skopeo/delete.go +++ b/cmd/skopeo/delete.go @@ -3,6 +3,7 @@ package main import ( "errors" "fmt" + "io" "strings" "github.com/containers/image/transports" @@ -34,19 +35,19 @@ func deleteCmd(global *globalOptions) cli.Command { See skopeo(1) section "IMAGE NAMES" for the expected format `, strings.Join(transports.ListNames(), ", ")), ArgsUsage: "IMAGE-NAME", - Action: opts.run, + Action: commandAction(opts.run), Flags: append(sharedFlags, imageFlags...), } } -func (opts *deleteOptions) run(c *cli.Context) error { - if len(c.Args()) != 1 { +func (opts *deleteOptions) run(args []string, stdout io.Writer) error { + if len(args) != 1 { return errors.New("Usage: delete imageReference") } - ref, err := alltransports.ParseImageName(c.Args()[0]) + ref, err := alltransports.ParseImageName(args[0]) if err != nil { - return fmt.Errorf("Invalid source name %s: %v", c.Args()[0], err) + return fmt.Errorf("Invalid source name %s: %v", args[0], err) } sys, err := opts.image.newSystemContext() diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index 9cd7b3dd..a4418df5 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "io" "strings" "time" @@ -61,15 +62,18 @@ func inspectCmd(global *globalOptions) cli.Command { Destination: &opts.raw, }, }, sharedFlags...), imageFlags...), - Action: opts.run, + Action: commandAction(opts.run), } } -func (opts *inspectOptions) run(c *cli.Context) (retErr error) { +func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) { ctx, cancel := opts.global.commandTimeoutContext() defer cancel() - img, err := parseImage(ctx, opts.image, c.Args().First()) + if len(args) != 1 { + return errors.New("Exactly one argument expected") + } + img, err := parseImage(ctx, opts.image, args[0]) if err != nil { return err } @@ -85,7 +89,7 @@ func (opts *inspectOptions) run(c *cli.Context) (retErr error) { return err } if opts.raw { - _, err := c.App.Writer.Write(rawManifest) + _, err := stdout.Write(rawManifest) if err != nil { return fmt.Errorf("Error writing manifest to standard output: %v", err) } @@ -134,6 +138,6 @@ func (opts *inspectOptions) run(c *cli.Context) (retErr error) { if err != nil { return err } - fmt.Fprintln(c.App.Writer, string(out)) + fmt.Fprintln(stdout, string(out)) return nil } diff --git a/cmd/skopeo/layers.go b/cmd/skopeo/layers.go index aa8b296a..5c236dc3 100644 --- a/cmd/skopeo/layers.go +++ b/cmd/skopeo/layers.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io" "io/ioutil" "os" "strings" @@ -32,14 +33,14 @@ func layersCmd(global *globalOptions) cli.Command { Usage: "Get layers of IMAGE-NAME", ArgsUsage: "IMAGE-NAME [LAYER...]", Hidden: true, - Action: opts.run, + Action: commandAction(opts.run), Flags: append(sharedFlags, imageFlags...), } } -func (opts *layersOptions) run(c *cli.Context) (retErr error) { +func (opts *layersOptions) run(args []string, stdout io.Writer) (retErr error) { fmt.Fprintln(os.Stderr, `DEPRECATED: skopeo layers is deprecated in favor of skopeo copy`) - if c.NArg() == 0 { + if len(args) == 0 { return errors.New("Usage: layers imageReference [layer...]") } @@ -51,7 +52,7 @@ func (opts *layersOptions) run(c *cli.Context) (retErr error) { return err } cache := blobinfocache.DefaultCache(sys) - rawSource, err := parseImageSource(ctx, opts.image, c.Args()[0]) + rawSource, err := parseImageSource(ctx, opts.image, args[0]) if err != nil { return err } @@ -74,7 +75,7 @@ func (opts *layersOptions) run(c *cli.Context) (retErr error) { isConfig bool } var blobDigests []blobDigest - for _, dString := range c.Args().Tail() { + for _, dString := range args[1:] { if !strings.HasPrefix(dString, "sha256:") { dString = "sha256:" + dString } diff --git a/cmd/skopeo/manifest.go b/cmd/skopeo/manifest.go index 177fefe5..5ada000a 100644 --- a/cmd/skopeo/manifest.go +++ b/cmd/skopeo/manifest.go @@ -3,6 +3,7 @@ package main import ( "errors" "fmt" + "io" "io/ioutil" "github.com/containers/image/manifest" @@ -18,15 +19,15 @@ func manifestDigestCmd() cli.Command { Name: "manifest-digest", Usage: "Compute a manifest digest of a file", ArgsUsage: "MANIFEST", - Action: opts.run, + Action: commandAction(opts.run), } } -func (opts *manifestDigestOptions) run(context *cli.Context) error { - if len(context.Args()) != 1 { +func (opts *manifestDigestOptions) run(args []string, stdout io.Writer) error { + if len(args) != 1 { return errors.New("Usage: skopeo manifest-digest manifest") } - manifestPath := context.Args()[0] + manifestPath := args[0] man, err := ioutil.ReadFile(manifestPath) if err != nil { @@ -36,6 +37,6 @@ func (opts *manifestDigestOptions) run(context *cli.Context) error { if err != nil { return fmt.Errorf("Error computing digest: %v", err) } - fmt.Fprintf(context.App.Writer, "%s\n", digest) + fmt.Fprintf(stdout, "%s\n", digest) return nil } diff --git a/cmd/skopeo/signing.go b/cmd/skopeo/signing.go index 9d7a29a5..6affd643 100644 --- a/cmd/skopeo/signing.go +++ b/cmd/skopeo/signing.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "github.com/containers/image/signature" @@ -20,7 +21,7 @@ func standaloneSignCmd() cli.Command { Name: "standalone-sign", Usage: "Create a signature using local files", ArgsUsage: "MANIFEST DOCKER-REFERENCE KEY-FINGERPRINT", - Action: opts.run, + Action: commandAction(opts.run), Flags: []cli.Flag{ cli.StringFlag{ Name: "output, o", @@ -31,13 +32,13 @@ func standaloneSignCmd() cli.Command { } } -func (opts *standaloneSignOptions) run(c *cli.Context) error { - if len(c.Args()) != 3 || opts.output == "" { +func (opts *standaloneSignOptions) run(args []string, stdout io.Writer) error { + if len(args) != 3 || opts.output == "" { return errors.New("Usage: skopeo standalone-sign manifest docker-reference key-fingerprint -o signature") } - manifestPath := c.Args()[0] - dockerReference := c.Args()[1] - fingerprint := c.Args()[2] + manifestPath := args[0] + dockerReference := args[1] + fingerprint := args[2] manifest, err := ioutil.ReadFile(manifestPath) if err != nil { @@ -69,18 +70,18 @@ func standaloneVerifyCmd() cli.Command { Name: "standalone-verify", Usage: "Verify a signature using local files", ArgsUsage: "MANIFEST DOCKER-REFERENCE KEY-FINGERPRINT SIGNATURE", - Action: opts.run, + Action: commandAction(opts.run), } } -func (opts *standaloneVerifyOptions) run(c *cli.Context) error { - if len(c.Args()) != 4 { +func (opts *standaloneVerifyOptions) run(args []string, stdout io.Writer) error { + if len(args) != 4 { return errors.New("Usage: skopeo standalone-verify manifest docker-reference key-fingerprint signature") } - manifestPath := c.Args()[0] - expectedDockerReference := c.Args()[1] - expectedFingerprint := c.Args()[2] - signaturePath := c.Args()[3] + manifestPath := args[0] + expectedDockerReference := args[1] + expectedFingerprint := args[2] + signaturePath := args[3] unverifiedManifest, err := ioutil.ReadFile(manifestPath) if err != nil { @@ -101,7 +102,7 @@ func (opts *standaloneVerifyOptions) run(c *cli.Context) error { return fmt.Errorf("Error verifying signature: %v", err) } - fmt.Fprintf(c.App.Writer, "Signature verified, digest %s\n", sig.DockerManifestDigest) + fmt.Fprintf(stdout, "Signature verified, digest %s\n", sig.DockerManifestDigest) return nil } @@ -121,15 +122,15 @@ func untrustedSignatureDumpCmd() cli.Command { Usage: "Dump contents of a signature WITHOUT VERIFYING IT", ArgsUsage: "SIGNATURE", Hidden: true, - Action: opts.run, + Action: commandAction(opts.run), } } -func (opts *untrustedSignatureDumpOptions) run(c *cli.Context) error { - if len(c.Args()) != 1 { +func (opts *untrustedSignatureDumpOptions) run(args []string, stdout io.Writer) error { + if len(args) != 1 { return errors.New("Usage: skopeo untrusted-signature-dump-without-verification signature") } - untrustedSignaturePath := c.Args()[0] + untrustedSignaturePath := args[0] untrustedSignature, err := ioutil.ReadFile(untrustedSignaturePath) if err != nil { @@ -144,6 +145,6 @@ func (opts *untrustedSignatureDumpOptions) run(c *cli.Context) error { if err != nil { return err } - fmt.Fprintln(c.App.Writer, string(untrustedOut)) + fmt.Fprintln(stdout, string(untrustedOut)) return nil } diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index 66e0e6f0..d2636479 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "io" "strings" "github.com/containers/image/transports/alltransports" @@ -10,6 +11,26 @@ import ( "github.com/urfave/cli" ) +// errorShouldDisplayUsage is a subtype of error used by command handlers to indicate that cli.ShowSubcommandHelp should be called. +type errorShouldDisplayUsage struct { + error +} + +// commandAction intermediates between the cli.ActionFunc interface and the real handler, +// primarily to ensure that cli.Context is not available to the handler, which in turn +// makes sure that the cli.String() etc. flag access functions are not used, +// and everything is done using the *Options structures and the Destination: members of cli.Flag. +// handler may return errorShouldDisplayUsage to cause cli.ShowSubcommandHelp to be called. +func commandAction(handler func(args []string, stdout io.Writer) error) cli.ActionFunc { + return func(c *cli.Context) error { + err := handler(([]string)(c.Args()), c.App.Writer) + if _, ok := err.(errorShouldDisplayUsage); ok { + cli.ShowSubcommandHelp(c) + } + return err + } +} + // sharedImageOptions collects CLI flags which are image-related, but do not change across images. // This really should be a part of globalOptions, but that would break existing users of (skopeo copy --authfile=). type sharedImageOptions struct { From 8fe1595f9268d0a3122ccf2779dd540048f752e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Trma=C4=8D?= Date: Thu, 12 Jul 2018 14:23:37 +0200 Subject: [PATCH 30/30] Do not interpret % metacharacters in (skopeo inspect) output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miloslav Trmač --- cmd/skopeo/inspect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index a4418df5..9c524b5f 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -138,6 +138,6 @@ func (opts *inspectOptions) run(args []string, stdout io.Writer) (retErr error) if err != nil { return err } - fmt.Fprintln(stdout, string(out)) + fmt.Fprintf(stdout, "%s\n", string(out)) return nil }