2020-04-24 18:04:32 +02:00
/ *
2020-09-22 12:13:00 +02:00
Copyright 2020 Docker Compose CLI authors
2020-06-18 16:13:24 +02:00
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
2020-04-24 18:04:32 +02:00
* /
package main
import (
"context"
"fmt"
2020-05-22 10:45:01 +02:00
"math/rand"
2020-04-24 18:04:32 +02:00
"os"
2020-05-13 10:24:16 +02:00
"os/signal"
2020-04-24 18:04:32 +02:00
"path/filepath"
2020-06-05 17:30:27 +02:00
"regexp"
2020-10-29 15:22:44 +01:00
"strings"
2020-05-13 10:24:16 +02:00
"syscall"
2020-05-22 10:45:01 +02:00
"time"
2020-04-24 18:04:32 +02:00
2021-05-19 15:47:12 +02:00
"github.com/compose-spec/compose-go/types"
2021-04-07 11:39:31 +02:00
"github.com/docker/cli/cli"
2020-09-11 16:08:06 +02:00
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
2021-03-11 11:43:40 +01:00
"github.com/docker/compose-cli/api/backend"
2021-01-15 16:55:10 +01:00
"github.com/docker/compose-cli/api/config"
2021-01-15 16:31:59 +01:00
apicontext "github.com/docker/compose-cli/api/context"
"github.com/docker/compose-cli/api/context/store"
2020-08-21 17:24:53 +02:00
"github.com/docker/compose-cli/cli/cmd"
contextcmd "github.com/docker/compose-cli/cli/cmd/context"
"github.com/docker/compose-cli/cli/cmd/login"
2020-09-25 14:27:50 +02:00
"github.com/docker/compose-cli/cli/cmd/logout"
2020-08-21 17:24:53 +02:00
"github.com/docker/compose-cli/cli/cmd/run"
2020-10-07 15:33:27 +02:00
"github.com/docker/compose-cli/cli/cmd/volume"
2021-03-04 19:21:42 +01:00
cliconfig "github.com/docker/compose-cli/cli/config"
2021-01-15 15:26:35 +01:00
"github.com/docker/compose-cli/cli/metrics"
2020-08-21 17:24:53 +02:00
"github.com/docker/compose-cli/cli/mobycli"
cliopts "github.com/docker/compose-cli/cli/options"
2021-06-15 08:48:49 +02:00
compose2 "github.com/docker/compose-cli/cmd/compose"
2021-03-11 11:43:40 +01:00
"github.com/docker/compose-cli/local"
2021-06-15 08:48:49 +02:00
"github.com/docker/compose-cli/pkg/api"
2021-06-15 09:57:38 +02:00
"github.com/docker/compose-cli/pkg/compose"
2021-03-02 15:21:54 +01:00
2020-09-25 14:27:50 +02:00
// Backend registrations
_ "github.com/docker/compose-cli/aci"
2020-09-10 14:17:49 +02:00
_ "github.com/docker/compose-cli/ecs"
_ "github.com/docker/compose-cli/ecs/local"
_ "github.com/docker/compose-cli/local"
2020-04-24 18:04:32 +02:00
)
2020-05-12 14:35:16 +02:00
var (
2020-09-04 10:19:40 +02:00
contextAgnosticCommands = map [ string ] struct { } {
2020-12-22 16:53:17 +01:00
"context" : { } ,
"login" : { } ,
"logout" : { } ,
"serve" : { } ,
"version" : { } ,
"backend-metadata" : { } ,
2020-05-29 17:07:48 +02:00
}
2021-04-06 23:19:55 +02:00
unknownCommandRegexp = regexp . MustCompile ( ` unknown docker command: "([^"]*)" ` )
2020-05-12 14:35:16 +02:00
)
2020-04-24 18:04:32 +02:00
func init ( ) {
// initial hack to get the path of the project's bin dir
// into the env of this cli for development
path , err := filepath . Abs ( filepath . Dir ( os . Args [ 0 ] ) )
if err != nil {
2020-05-14 09:59:12 +02:00
fatal ( errors . Wrap ( err , "unable to get absolute bin path" ) )
2020-04-24 18:04:32 +02:00
}
2020-10-29 15:22:44 +01:00
if err := os . Setenv ( "PATH" , appendPaths ( os . Getenv ( "PATH" ) , path ) ) ; err != nil {
2020-04-24 18:04:32 +02:00
panic ( err )
}
2020-05-22 10:45:01 +02:00
// Seed random
rand . Seed ( time . Now ( ) . UnixNano ( ) )
2020-04-24 18:04:32 +02:00
}
2020-10-29 15:22:44 +01:00
func appendPaths ( envPath string , path string ) string {
if envPath == "" {
return path
}
return strings . Join ( [ ] string { envPath , path } , string ( os . PathListSeparator ) )
}
2020-09-04 10:19:40 +02:00
func isContextAgnosticCommand ( cmd * cobra . Command ) bool {
2020-04-26 22:13:35 +02:00
if cmd == nil {
return false
}
2020-09-04 10:19:40 +02:00
if _ , ok := contextAgnosticCommands [ cmd . Name ( ) ] ; ok {
2020-04-26 22:13:35 +02:00
return true
}
2020-09-04 10:19:40 +02:00
return isContextAgnosticCommand ( cmd . Parent ( ) )
2020-04-26 22:13:35 +02:00
}
2020-04-24 18:04:32 +02:00
func main ( ) {
2020-05-14 18:29:09 +02:00
var opts cliopts . GlobalOpts
2020-04-24 18:04:32 +02:00
root := & cobra . Command {
2021-02-03 14:02:12 +01:00
Use : "docker" ,
SilenceErrors : true ,
SilenceUsage : true ,
TraverseChildren : true ,
2020-04-24 18:04:32 +02:00
PersistentPreRunE : func ( cmd * cobra . Command , args [ ] string ) error {
2020-09-04 10:19:40 +02:00
if ! isContextAgnosticCommand ( cmd ) {
2020-09-16 17:22:21 +02:00
mobycli . ExecIfDefaultCtxType ( cmd . Context ( ) , cmd . Root ( ) )
2020-04-26 22:13:35 +02:00
}
2020-04-24 18:04:32 +02:00
return nil
} ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2021-02-03 14:02:12 +01:00
if len ( args ) == 0 {
return cmd . Help ( )
}
2021-04-06 23:19:55 +02:00
return fmt . Errorf ( "unknown docker command: %q" , args [ 0 ] )
2020-04-24 18:04:32 +02:00
} ,
}
2020-05-01 15:28:44 +02:00
root . AddCommand (
2020-05-20 15:55:05 +02:00
contextcmd . Command ( ) ,
2020-05-06 09:37:52 +02:00
cmd . PsCommand ( ) ,
2020-05-01 15:28:44 +02:00
cmd . ServeCommand ( ) ,
2020-05-03 13:35:25 +02:00
cmd . ExecCommand ( ) ,
2020-05-03 13:41:45 +02:00
cmd . LogsCommand ( ) ,
2020-05-10 22:37:28 +02:00
cmd . RmCommand ( ) ,
2020-08-12 10:35:15 +02:00
cmd . StartCommand ( ) ,
2020-06-12 10:53:06 +02:00
cmd . InspectCommand ( ) ,
2020-05-29 17:07:48 +02:00
login . Command ( ) ,
2020-07-08 16:01:54 +02:00
logout . Command ( ) ,
2020-10-07 15:33:27 +02:00
cmd . VersionCommand ( ) ,
2020-08-11 17:49:02 +02:00
cmd . StopCommand ( ) ,
2020-09-01 16:06:07 +02:00
cmd . KillCommand ( ) ,
2020-08-20 15:55:55 +02:00
cmd . SecretCommand ( ) ,
2020-10-14 16:06:45 +02:00
cmd . PruneCommand ( ) ,
2020-12-22 16:53:17 +01:00
cmd . MetadataCommand ( ) ,
2020-08-14 10:20:02 +02:00
// Place holders
cmd . EcsCommand ( ) ,
2020-05-01 15:28:44 +02:00
)
2020-04-24 18:04:32 +02:00
helpFunc := root . HelpFunc ( )
root . SetHelpFunc ( func ( cmd * cobra . Command , args [ ] string ) {
2020-09-04 10:19:40 +02:00
if ! isContextAgnosticCommand ( cmd ) {
2020-09-16 17:22:21 +02:00
mobycli . ExecIfDefaultCtxType ( cmd . Context ( ) , cmd . Root ( ) )
2020-04-26 22:13:35 +02:00
}
2020-04-24 18:04:32 +02:00
helpFunc ( cmd , args )
} )
2021-02-03 14:02:12 +01:00
flags := root . Flags ( )
2021-03-02 15:21:54 +01:00
opts . InstallFlags ( flags )
2021-02-03 14:02:12 +01:00
opts . AddConfigFlags ( flags )
flags . BoolVarP ( & opts . Version , "version" , "v" , false , "Print version information and quit" )
2020-04-24 18:04:32 +02:00
2021-02-15 09:56:34 +01:00
flags . SetInterspersed ( false )
2020-10-04 02:43:18 +02:00
walk ( root , func ( c * cobra . Command ) {
c . Flags ( ) . BoolP ( "help" , "h" , false , "Help for " + c . Name ( ) )
} )
2020-04-24 18:04:32 +02:00
// populate the opts with the global flags
2021-02-03 14:02:12 +01:00
flags . Parse ( os . Args [ 1 : ] ) //nolint: errcheck
2021-02-03 10:40:34 +01:00
level , err := logrus . ParseLevel ( opts . LogLevel )
if err != nil {
fmt . Fprintf ( os . Stderr , "Unable to parse logging level: %s\n" , opts . LogLevel )
os . Exit ( 1 )
}
2021-02-10 14:20:32 +01:00
logrus . SetFormatter ( & logrus . TextFormatter {
DisableTimestamp : true ,
DisableLevelTruncation : true ,
} )
2021-02-03 10:40:34 +01:00
logrus . SetLevel ( level )
2020-05-14 18:29:09 +02:00
if opts . Debug {
2020-04-24 18:04:32 +02:00
logrus . SetLevel ( logrus . DebugLevel )
}
2020-05-13 10:24:16 +02:00
ctx , cancel := newSigContext ( )
2020-04-24 18:04:32 +02:00
defer cancel ( )
2021-03-02 15:21:54 +01:00
// --version should immediately be forwarded to the original cli
if opts . Version {
2020-09-16 17:22:21 +02:00
mobycli . Exec ( root )
2020-06-23 15:13:43 +02:00
}
2020-06-23 11:27:19 +02:00
2020-05-14 18:29:09 +02:00
if opts . Config == "" {
fatal ( errors . New ( "config path cannot be empty" ) )
}
2020-05-20 15:55:05 +02:00
configDir := opts . Config
2021-03-11 11:43:40 +01:00
config . WithDir ( configDir )
2020-05-18 14:38:21 +02:00
2021-03-04 19:21:42 +01:00
currentContext := cliconfig . GetCurrentContext ( opts . Context , configDir , opts . Hosts )
2021-03-11 11:43:40 +01:00
apicontext . WithCurrentContext ( currentContext )
2020-04-29 23:10:06 +02:00
2020-09-16 15:53:25 +02:00
s , err := store . New ( configDir )
2020-04-24 18:04:32 +02:00
if err != nil {
2020-09-16 17:22:21 +02:00
mobycli . Exec ( root )
2020-04-24 18:04:32 +02:00
}
2021-03-11 11:43:40 +01:00
store . WithContextStore ( s )
2020-06-22 09:55:28 +02:00
ctype := store . DefaultContextType
cc , _ := s . Get ( currentContext )
if cc != nil {
ctype = cc . Type ( )
}
2021-03-11 11:43:40 +01:00
service , err := getBackend ( ctype , configDir , opts )
if err != nil {
fatal ( err )
}
backend . WithBackend ( service )
2020-09-21 17:05:49 +02:00
root . AddCommand (
run . Command ( ctype ) ,
2020-10-19 17:35:40 +02:00
volume . Command ( ctype ) ,
2020-09-21 17:05:49 +02:00
)
2021-03-11 11:43:40 +01:00
2021-04-23 11:54:42 +02:00
if ctype != store . DefaultContextType {
// On default context, "compose" is implemented by CLI Plugin
2021-05-19 15:47:12 +02:00
proxy := api . NewServiceProxy ( ) . WithService ( service . ComposeService ( ) )
2021-06-15 08:48:49 +02:00
command := compose2 . RootCommand ( ctype , proxy )
2021-05-19 15:47:12 +02:00
if ctype == store . AciContextType {
customizeCliForACI ( command , proxy )
}
root . AddCommand ( command )
2021-04-23 11:54:42 +02:00
}
2021-03-11 11:43:40 +01:00
if err = root . ExecuteContext ( ctx ) ; err != nil {
handleError ( ctx , err , ctype , currentContext , cc , root )
}
2021-06-15 09:57:38 +02:00
metrics . Track ( ctype , os . Args [ 1 : ] , compose . SuccessStatus )
2021-03-11 11:43:40 +01:00
}
2021-05-19 15:47:12 +02:00
func customizeCliForACI ( command * cobra . Command , proxy * api . ServiceProxy ) {
var domainName string
for _ , c := range command . Commands ( ) {
if c . Name ( ) == "up" {
c . Flags ( ) . StringVar ( & domainName , "domainname" , "" , "Container NIS domain name" )
proxy . WithInterceptor ( func ( ctx context . Context , project * types . Project ) {
if domainName != "" {
// arbitrarily set the domain name on the first service ; ACI backend will expose the entire project
project . Services [ 0 ] . DomainName = domainName
}
} )
}
}
}
2021-03-11 11:43:40 +01:00
func getBackend ( ctype string , configDir string , opts cliopts . GlobalOpts ) ( backend . Service , error ) {
switch ctype {
case store . DefaultContextType , store . LocalContextType :
2021-03-04 19:21:42 +01:00
return local . GetLocalBackend ( configDir , opts )
2021-03-02 15:21:54 +01:00
}
2021-03-11 11:43:40 +01:00
service , err := backend . Get ( ctype )
2021-06-14 16:26:14 +02:00
if api . IsNotFoundError ( err ) {
2021-03-11 11:43:40 +01:00
return service , nil
2021-02-03 14:02:12 +01:00
}
2021-03-11 11:43:40 +01:00
return service , err
2021-02-03 14:02:12 +01:00
}
2020-06-05 17:30:27 +02:00
2021-02-03 14:02:12 +01:00
func handleError ( ctx context . Context , err error , ctype string , currentContext string , cc * store . DockerContext , root * cobra . Command ) {
// if user canceled request, simply exit without any error message
2021-06-14 16:26:14 +02:00
if api . IsErrCanceled ( err ) || errors . Is ( ctx . Err ( ) , context . Canceled ) {
2021-06-15 09:57:38 +02:00
metrics . Track ( ctype , os . Args [ 1 : ] , compose . CanceledStatus )
2021-02-03 14:02:12 +01:00
os . Exit ( 130 )
}
if ctype == store . AwsContextType {
exit ( currentContext , errors . Errorf ( ` % q context type has been renamed . Recreate the context by running :
$ docker context create % s < name > ` , cc . Type ( ) , store . EcsContextType ) , ctype )
}
2020-08-11 15:46:29 +02:00
2021-02-03 14:02:12 +01:00
// Context should always be handled by new CLI
requiredCmd , _ , _ := root . Find ( os . Args [ 1 : ] )
if requiredCmd != nil && isContextAgnosticCommand ( requiredCmd ) {
2020-10-07 23:29:55 +02:00
exit ( currentContext , err , ctype )
2020-07-02 13:52:57 +02:00
}
2021-02-03 14:02:12 +01:00
mobycli . ExecIfDefaultCtxType ( ctx , root )
checkIfUnknownCommandExistInDefaultContext ( err , currentContext , ctype )
exit ( currentContext , err , ctype )
2020-07-02 13:52:57 +02:00
}
2020-10-07 23:29:55 +02:00
func exit ( ctx string , err error , ctype string ) {
2021-04-07 11:39:31 +02:00
if exit , ok := err . ( cli . StatusError ) ; ok {
2021-06-15 09:57:38 +02:00
metrics . Track ( ctype , os . Args [ 1 : ] , compose . SuccessStatus )
2021-04-07 11:39:31 +02:00
os . Exit ( exit . StatusCode )
2021-02-10 14:20:32 +01:00
}
2021-06-15 09:57:38 +02:00
var composeErr compose . Error
metricsStatus := compose . FailureStatus
2021-04-06 23:19:55 +02:00
exitCode := 1
if errors . As ( err , & composeErr ) {
metricsStatus = composeErr . GetMetricsFailureCategory ( ) . MetricsStatus
exitCode = composeErr . GetMetricsFailureCategory ( ) . ExitCode
}
if strings . HasPrefix ( err . Error ( ) , "unknown shorthand flag:" ) || strings . HasPrefix ( err . Error ( ) , "unknown flag:" ) || strings . HasPrefix ( err . Error ( ) , "unknown docker command:" ) {
2021-06-15 09:57:38 +02:00
metricsStatus = compose . CommandSyntaxFailure . MetricsStatus
exitCode = compose . CommandSyntaxFailure . ExitCode
2021-04-06 23:19:55 +02:00
}
metrics . Track ( ctype , os . Args [ 1 : ] , metricsStatus )
2020-09-16 17:22:21 +02:00
2021-06-14 16:26:14 +02:00
if errors . Is ( err , api . ErrLoginRequired ) {
2020-07-29 11:58:09 +02:00
fmt . Fprintln ( os . Stderr , err )
2021-06-14 16:26:14 +02:00
os . Exit ( api . ExitCodeLoginRequired )
2020-04-24 18:04:32 +02:00
}
2021-02-23 09:35:26 +01:00
2021-06-15 08:48:49 +02:00
if compose2 . Warning != "" {
2021-03-04 19:21:42 +01:00
logrus . Warn ( err )
2021-06-15 08:48:49 +02:00
fmt . Fprintln ( os . Stderr , compose2 . Warning )
2021-02-23 09:35:26 +01:00
}
2021-06-14 16:26:14 +02:00
if errors . Is ( err , api . ErrNotImplemented ) {
2020-10-07 23:29:55 +02:00
name := metrics . GetCommand ( os . Args [ 1 : ] )
2021-04-15 14:19:59 +02:00
fmt . Fprintf ( os . Stderr , "Command %q not available in current context (%s)\n" , name , ctx )
2020-09-30 10:44:10 +02:00
2020-08-20 09:06:17 +02:00
os . Exit ( 1 )
}
2021-04-06 23:19:55 +02:00
fmt . Fprintln ( os . Stderr , err )
os . Exit ( exitCode )
2020-04-24 18:04:32 +02:00
}
2020-08-11 15:46:29 +02:00
func fatal ( err error ) {
fmt . Fprintln ( os . Stderr , err )
os . Exit ( 1 )
}
2020-10-07 23:29:55 +02:00
func checkIfUnknownCommandExistInDefaultContext ( err error , currentContext string , contextType string ) {
2020-06-26 10:23:37 +02:00
submatch := unknownCommandRegexp . FindSubmatch ( [ ] byte ( err . Error ( ) ) )
2020-06-05 17:30:27 +02:00
if len ( submatch ) == 2 {
dockerCommand := string ( submatch [ 1 ] )
2020-06-17 17:57:44 +02:00
if mobycli . IsDefaultContextCommand ( dockerCommand ) {
2021-04-15 14:19:59 +02:00
fmt . Fprintf ( os . Stderr , "Command %q not available in current context (%s), you can use the \"default\" context to run this command\n" , dockerCommand , currentContext )
2021-06-15 09:57:38 +02:00
metrics . Track ( contextType , os . Args [ 1 : ] , compose . FailureStatus )
2020-06-05 17:30:27 +02:00
os . Exit ( 1 )
}
}
}
2020-05-13 10:24:16 +02:00
func newSigContext ( ) ( context . Context , func ( ) ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
2020-08-13 10:34:42 +02:00
s := make ( chan os . Signal , 1 )
2020-05-13 10:24:16 +02:00
signal . Notify ( s , syscall . SIGTERM , syscall . SIGINT )
go func ( ) {
<- s
cancel ( )
} ( )
return ctx , cancel
}
2020-10-04 02:43:18 +02:00
func walk ( c * cobra . Command , f func ( * cobra . Command ) ) {
f ( c )
for _ , c := range c . Commands ( ) {
walk ( c , f )
}
}