diff --git a/cmd/deploy.go b/cmd/deploy.go index 4571859290..e6462f7aec 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -1,6 +1,10 @@ package cmd import ( + "os" + + "gopkg.in/src-d/go-git.v4/plumbing" + "github.com/covexo/devspace/pkg/devspace/cloud" "github.com/covexo/devspace/pkg/devspace/config/configutil" "github.com/covexo/devspace/pkg/devspace/config/generated" @@ -11,6 +15,7 @@ import ( "github.com/covexo/devspace/pkg/devspace/registry" "github.com/covexo/devspace/pkg/util/log" "github.com/spf13/cobra" + git "gopkg.in/src-d/go-git.v4" ) // DeployCmd holds the required data for the down cmd @@ -26,6 +31,8 @@ type DeployCmdFlags struct { DockerTarget string CloudTarget string SwitchContext bool + SkipBuild bool + GitBranch string } func init() { @@ -47,8 +54,9 @@ devspace deploy --namespace=deploy --docker-target=production devspace deploy --kube-context=deploy-context devspace deploy --config=.devspace/deploy.yaml devspace deploy --cloud-target=production +devspace deploy https://github.com/covexo/devspace --branch test #######################################################`, - Args: cobra.NoArgs, + Args: cobra.RangeArgs(0, 2), Run: cmd.Run, } @@ -58,12 +66,37 @@ devspace deploy --cloud-target=production cobraCmd.Flags().StringVar(&cmd.flags.DockerTarget, "docker-target", "", "The docker target to use for building") cobraCmd.Flags().StringVar(&cmd.flags.CloudTarget, "cloud-target", "", "When using a cloud provider, the target to use") cobraCmd.Flags().BoolVar(&cmd.flags.SwitchContext, "switch-context", false, "Switches the kube context to the deploy context") + cobraCmd.Flags().BoolVar(&cmd.flags.SkipBuild, "skip-build", false, "Skips the image build & push step") + cobraCmd.Flags().StringVar(&cmd.flags.GitBranch, "branch", "master", "The git branch to checkout") rootCmd.AddCommand(cobraCmd) } // Run executes the down command logic func (cmd *DeployCmd) Run(cobraCmd *cobra.Command, args []string) { + if len(args) > 0 { + directoryName := "devspace" + if len(args) == 2 { + directoryName = args[1] + } + + _, err := git.PlainClone(directoryName, false, &git.CloneOptions{ + URL: args[0], + Progress: os.Stdout, + ReferenceName: plumbing.ReferenceName(cmd.flags.GitBranch), + }) + if err != nil { + log.Fatal(err) + } + + err = os.Chdir(directoryName) + if err != nil { + log.Fatal(err) + } + + log.Donef("Successfully checked out %s into %s", args[0], directoryName) + } + cloud.UseDeployTarget = true log.StartFileLogging() @@ -100,10 +133,12 @@ func (cmd *DeployCmd) Run(cobraCmd *cobra.Command, args []string) { log.Fatalf("Error loading generated.yaml: %v", err) } - // Force image build - _, err = image.BuildAll(client, generatedConfig, true, log.GetInstance()) - if err != nil { - log.Fatal(err) + if cmd.flags.SkipBuild == false { + // Force image build + _, err = image.BuildAll(client, generatedConfig, true, log.GetInstance()) + if err != nil { + log.Fatal(err) + } } // Force deployment of all defined deployments diff --git a/cmd/init.go b/cmd/init.go index 898139b35d..0deb946b5d 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -280,6 +280,7 @@ func (cmd *InitCmd) useCloudProvider() bool { return false } + func (cmd *InitCmd) loginToCloudProvider(providerConfig cloud.ProviderConfig, cloudProviderSelected string) { config := configutil.GetConfig() addToContext := cmd.flags.skipQuestions || cmd.flags.addDevSpaceCloudToLocalKubernetes @@ -296,6 +297,7 @@ func (cmd *InitCmd) loginToCloudProvider(providerConfig cloud.ProviderConfig, cl err := cloud.Update(providerConfig, &cloud.UpdateOptions{ UseKubeContext: addToContext, SwitchKubeContext: true, + SkipSaveConfig: true, }, log.GetInstance()) if err != nil { log.Fatalf("Couldn't authenticate to %s: %v", cloudProviderSelected, err) diff --git a/main.go b/main.go index a4baf6cdbf..0067b066a6 100644 --- a/main.go +++ b/main.go @@ -1,18 +1,24 @@ package main import ( - "os" + "fmt" - "github.com/covexo/devspace/cmd" - "github.com/covexo/devspace/pkg/devspace/upgrade" + "github.com/covexo/devspace/pkg/util/stdinutil" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" ) var version string func main() { - upgrade.SetVersion(version) + /*upgrade.SetVersion(version) cmd.Execute() - os.Exit(0) + os.Exit(0)*/ + useDevSpaceCloud := *stdinutil.GetFromStdin(&stdinutil.GetFromStdinParams{ + Question: "Do you want to use the DevSpace Cloud? (free ready-to-use Kubernetes) (yes | no)", + DefaultValue: "yes", + ValidationRegexPattern: "^(yes)|(no)$", + }) + + fmt.Println(useDevSpaceCloud) } diff --git a/pkg/devspace/builder/docker/auth.go b/pkg/devspace/builder/docker/auth.go index d7c1be3b38..d22c6b528a 100644 --- a/pkg/devspace/builder/docker/auth.go +++ b/pkg/devspace/builder/docker/auth.go @@ -4,7 +4,6 @@ import ( "context" "encoding/base64" "encoding/json" - "fmt" "github.com/covexo/devspace/pkg/util/log" "github.com/docker/docker/api/types" @@ -12,7 +11,7 @@ import ( "github.com/docker/docker/registry" ) -func getOfficialServer(ctx context.Context, client client.CommonAPIClient) string { +func getOfficialServer(ctx context.Context, client client.CommonAPIClient, log log.Logger) string { // The daemon `/info` endpoint informs us of the default registry being // used. This is essential in cross-platforms environment, where for // example a Linux client might be interacting with a Windows daemon, hence @@ -20,9 +19,9 @@ func getOfficialServer(ctx context.Context, client client.CommonAPIClient) strin serverAddress := registry.IndexServer if info, err := client.Info(ctx); err != nil { // Only report the warning if we're in debug mode to prevent nagging during engine initialization workflows - fmt.Fprintf(log.GetInstance(), "Warning: failed to get default registry endpoint from daemon (%v). Using system default: %s\n", err, serverAddress) + // log.Warnf("Warning: failed to get default registry endpoint from daemon (%v). Using system default: %s", err, serverAddress) } else if info.IndexServerAddress == "" { - fmt.Fprintf(log.GetInstance(), "Warning: Empty registry endpoint from daemon. Using system default: %s\n", serverAddress) + // log.Warnf("Warning: Empty registry endpoint from daemon. Using system default: %s", serverAddress) } else { serverAddress = info.IndexServerAddress } diff --git a/pkg/devspace/builder/docker/config.go b/pkg/devspace/builder/docker/config.go index 6b3ce7aba9..cebc856523 100644 --- a/pkg/devspace/builder/docker/config.go +++ b/pkg/devspace/builder/docker/config.go @@ -15,13 +15,11 @@ const dockerFileFolder = ".docker" var configDir = os.Getenv("DOCKER_CONFIG") -func init() { +func loadDockerConfig() (*configfile.ConfigFile, error) { if configDir == "" { configDir = filepath.Join(homedir.Get(), dockerFileFolder) } -} -func loadDockerConfig() (*configfile.ConfigFile, error) { return config.Load(configDir) } diff --git a/pkg/devspace/builder/docker/docker.go b/pkg/devspace/builder/docker/docker.go index 75d852eff2..54b10987f4 100644 --- a/pkg/devspace/builder/docker/docker.go +++ b/pkg/devspace/builder/docker/docker.go @@ -1,10 +1,12 @@ package docker import ( + "fmt" "strings" "context" + "github.com/covexo/devspace/pkg/util/log" "github.com/docker/distribution/reference" "github.com/docker/docker/pkg/term" "github.com/docker/docker/registry" @@ -149,47 +151,11 @@ func (b *Builder) BuildImage(contextPath, dockerfilePath string, options *types. // Authenticate authenticates the client with a remote registry func (b *Builder) Authenticate(user, password string, checkCredentialsStore bool) (*types.AuthConfig, error) { - ctx := context.Background() - authServer := getOfficialServer(ctx, b.client) - serverAddress := b.RegistryURL - - if serverAddress == "" { - serverAddress = authServer - } else { - ref, err := reference.ParseNormalizedNamed(b.imageURL) - if err != nil { - return nil, err - } - - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return nil, err - } - - if repoInfo.Index.Official { - serverAddress = authServer - } - } - - authConfig, err := getDefaultAuthConfig(b.client, checkCredentialsStore, serverAddress, serverAddress == authServer) - - if err != nil || authConfig.Username == "" || authConfig.Password == "" { - authConfig.Username = strings.TrimSpace(user) - authConfig.Password = strings.TrimSpace(password) - } - - response, err := b.client.RegistryLogin(ctx, *authConfig) + authConfig, err := b.Login(user, password, checkCredentialsStore, false) if err != nil { return nil, err } - if response.IdentityToken != "" { - authConfig.Password = "" - authConfig.IdentityToken = response.IdentityToken - } - - b.authConfig = authConfig - // Cache authConfig for GetAuthConfig authConfigs[b.RegistryURL] = authConfig @@ -232,7 +198,6 @@ func GetAuthConfig(registryURL string) (*types.AuthConfig, error) { } authConfig, authConfigExists := authConfigs[registryURL] - if !authConfigExists { dockerBuilder, err := NewBuilder(registryURL, "", "", false) if err != nil { @@ -244,5 +209,67 @@ func GetAuthConfig(registryURL string) (*types.AuthConfig, error) { return nil, err } } + return authConfig, nil } + +// Login logs the user into docker +func (b *Builder) Login(user, password string, checkCredentialsStore, saveAuthConfig bool) (*types.AuthConfig, error) { + ctx := context.Background() + authServer := getOfficialServer(ctx, b.client, log.GetInstance()) + serverAddress := b.RegistryURL + + if serverAddress == "" { + serverAddress = authServer + } else { + ref, err := reference.ParseNormalizedNamed(b.imageURL) + if err != nil { + return nil, err + } + + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return nil, err + } + + if repoInfo.Index.Official { + serverAddress = authServer + } + } + + authConfig, err := getDefaultAuthConfig(b.client, checkCredentialsStore, serverAddress, serverAddress == authServer) + if err != nil || authConfig.Username == "" || authConfig.Password == "" { + authConfig.Username = strings.TrimSpace(user) + authConfig.Password = strings.TrimSpace(password) + } + + response, err := b.client.RegistryLogin(ctx, *authConfig) + if err != nil { + return nil, err + } + + if response.IdentityToken != "" { + authConfig.Password = "" + authConfig.IdentityToken = response.IdentityToken + } + + if saveAuthConfig { + configfile, err := loadDockerConfig() + if err != nil { + return nil, err + } + + err = configfile.GetCredentialsStore(serverAddress).Store(*authConfig) + if err != nil { + return nil, fmt.Errorf("Error saving auth info in credentials store: %v", err) + } + + err = configfile.Save() + if err != nil { + return nil, fmt.Errorf("Error saving docker config: %v", err) + } + } + + b.authConfig = authConfig + return b.authConfig, nil +} diff --git a/pkg/devspace/cloud/update.go b/pkg/devspace/cloud/update.go index efde60846d..fceadce594 100644 --- a/pkg/devspace/cloud/update.go +++ b/pkg/devspace/cloud/update.go @@ -23,6 +23,7 @@ var UseDeployTarget = false type UpdateOptions struct { UseKubeContext bool SwitchKubeContext bool + SkipSaveConfig bool } // Update updates the cloud provider information if necessary @@ -52,9 +53,6 @@ func Update(providerConfig ProviderConfig, options *UpdateOptions, log log.Logge if dsConfig.Cluster.Namespace != nil { devSpaceID = *dsConfig.Cluster.Namespace } - if devSpaceID == "" && target != "" { - return fmt.Errorf("Cannot deploy to target %s without a devspace. You need to run `devspace up` beforehand", target) - } domain, namespace, cluster, authInfo, err := CheckAuth(provider, devSpaceID, target, log) if err != nil { @@ -68,7 +66,7 @@ func Update(providerConfig ProviderConfig, options *UpdateOptions, log log.Logge DevSpaceURL = domain - err = updateDevSpaceConfig(target, namespace, cluster, authInfo, options) + err = updateDevSpaceConfig(devSpaceID, target, namespace, cluster, authInfo, options) if err != nil { return err } @@ -76,7 +74,7 @@ func Update(providerConfig ProviderConfig, options *UpdateOptions, log log.Logge return nil } -func updateDevSpaceConfig(target, namespace string, cluster *api.Cluster, authInfo *api.AuthInfo, options *UpdateOptions) error { +func updateDevSpaceConfig(devSpaceID, target, namespace string, cluster *api.Cluster, authInfo *api.AuthInfo, options *UpdateOptions) error { dsConfig := configutil.GetConfig() overwriteConfig := configutil.GetOverwriteConfig() saveConfig := false @@ -143,7 +141,8 @@ func updateDevSpaceConfig(target, namespace string, cluster *api.Cluster, authIn } } - if saveConfig && target == "" { + // Either save when config has changed && devspace up or devspace deploy (with no up before) + if options.SkipSaveConfig == false && saveConfig && (target == "" || (target != "" && devSpaceID == "")) { err := configutil.SaveConfig() if err != nil { return err diff --git a/pkg/devspace/configure/registry.go b/pkg/devspace/configure/registry.go index ab6d71c7f8..b48826d6fe 100644 --- a/pkg/devspace/configure/registry.go +++ b/pkg/devspace/configure/registry.go @@ -43,7 +43,36 @@ func Image(dockerUsername string, skipQuestions bool, registryURL, defaultImageN dockerUsername = dockerAuthConfig.Username } } else if dockerUsername == "" { - return fmt.Errorf("Make sure you login to docker hub with: docker login") + log.Warn("No docker credentials were found in the credentials store") + log.Warn("Please make sure you have a https://hub.docker.com account") + + for { + dockerUsername = *stdinutil.GetFromStdin(&stdinutil.GetFromStdinParams{ + Question: "What is your docker hub username?", + DefaultValue: "", + ValidationRegexPattern: "^.*$", + }) + + dockerPassword := *stdinutil.GetFromStdin(&stdinutil.GetFromStdinParams{ + Question: "What is your docker hub password?", + DefaultValue: "", + ValidationRegexPattern: "^.*$", + IsPassword: true, + }) + + builder, err := docker.NewBuilder("", "", "", false) + if err != nil { + return err + } + + _, err = builder.Login(dockerUsername, dockerPassword, false, true) + if err != nil { + log.Warn(err) + continue + } + + break + } } googleRegistryRegex := regexp.MustCompile("^(.+\\.)?gcr.io$") diff --git a/pkg/devspace/sync/downstream.go b/pkg/devspace/sync/downstream.go index 652052b3c7..c54caf3e2f 100644 --- a/pkg/devspace/sync/downstream.go +++ b/pkg/devspace/sync/downstream.go @@ -165,8 +165,13 @@ func (d *downstream) collectChanges(removeFiles map[string]*fileInformation) ([] overlap := "" done := false + var downloadReader io.Reader = d.stdoutPipe + if d.config.DownstreamLimit > 0 { + downloadReader = ratelimit.Reader(d.stdoutPipe, ratelimit.NewBucketWithRate(float64(d.config.DownstreamLimit), d.config.DownstreamLimit)) + } + for done == false { - n, err := d.stdoutPipe.Read(buf[:cap(buf)]) + n, err := downloadReader.Read(buf[:cap(buf)]) buf = buf[:n] if n == 0 { diff --git a/pkg/util/stdinutil/stdin.go b/pkg/util/stdinutil/stdin.go index 5dabeabdaa..ee6cecebfa 100644 --- a/pkg/util/stdinutil/stdin.go +++ b/pkg/util/stdinutil/stdin.go @@ -11,6 +11,8 @@ import ( "github.com/daviddengcn/go-colortext" "github.com/covexo/devspace/pkg/util/paramutil" + "github.com/docker/cli/cli/command" + "github.com/docker/docker/pkg/term" ) //GetFromStdinParams defines a question and its answerpatterns @@ -19,6 +21,7 @@ type GetFromStdinParams struct { DefaultValue string ValidationRegexPattern string InputTerminationString string + IsPassword bool } var defaultParams = &GetFromStdinParams{ @@ -26,8 +29,6 @@ var defaultParams = &GetFromStdinParams{ InputTerminationString: "\n", } -var reader *bufio.Reader - const changeQuestion = "Would you like to change it? (yes, no/ENTER))" //GetFromStdin asks the user a question and returns the answer @@ -35,14 +36,6 @@ func GetFromStdin(params *GetFromStdinParams) *string { paramutil.SetDefaults(params, defaultParams) validationRegexp, _ := regexp.Compile(params.ValidationRegexPattern) - - if reader == nil { - reader = bufio.NewReader(os.Stdin) - - defer func() { - reader = nil - }() - } input := "" for { @@ -56,7 +49,24 @@ func GetFromStdin(params *GetFromStdinParams) *string { for { fmt.Print("> ") - nextLine, _ := reader.ReadString('\n') + + reader := bufio.NewReader(os.Stdin) + nextLine := "" + + if params.IsPassword { + inStreamFD := command.NewInStream(os.Stdin).FD() + oldState, err := term.SaveState(inStreamFD) + if err != nil { + log.Fatal(err) + } + + term.DisableEcho(inStreamFD, oldState) + nextLine, _ = reader.ReadString('\n') + term.RestoreTerminal(inStreamFD, oldState) + } else { + nextLine, _ = reader.ReadString('\n') + } + nextLine = strings.Trim(nextLine, "\r\n ") if strings.Compare(params.InputTerminationString, "\n") == 0 { @@ -86,38 +96,3 @@ func GetFromStdin(params *GetFromStdinParams) *string { return &input } - -//AskChangeQuestion asks two questions. Do you want to change this value? If yes, what's the new value? -func AskChangeQuestion(params *GetFromStdinParams) *string { - paramutil.SetDefaults(params, defaultParams) - - if reader == nil { - reader = bufio.NewReader(os.Stdin) - - defer func() { - reader = nil - }() - } - - shouldValueChangeQuestion := GetFromStdinParams{ - Question: params.Question + "\nThis is the current value:\n#################\n" + strings.TrimRight(params.DefaultValue, "\r\n") + "\n#################\n" + changeQuestion, - DefaultValue: "no", - ValidationRegexPattern: "yes|no", - } - - shouldChangeAnswer := GetFromStdin(&shouldValueChangeQuestion) - - if *shouldChangeAnswer == "no" { - return ¶ms.DefaultValue - } - - newValueQuestion := GetFromStdinParams{ - Question: "Please enter the new value:", - DefaultValue: params.DefaultValue, - ValidationRegexPattern: params.ValidationRegexPattern, - InputTerminationString: params.InputTerminationString, - } - - newValue := GetFromStdin(&newValueQuestion) - return newValue -} diff --git a/pkg/util/stdinutil/stdin_test.go b/pkg/util/stdinutil/stdin_test.go deleted file mode 100644 index efa86e0db5..0000000000 --- a/pkg/util/stdinutil/stdin_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package stdinutil - -import ( - "io/ioutil" - "os" - "testing" - - "github.com/juju/errors" -) - -func TestGetFromStdin_NoChangeQuestion_Default(t *testing.T) { - params := GetFromStdinParams{ - Question: "Is this a test?", - DefaultValue: "Yes", - ValidationRegexPattern: "No|Yes", - } - - err := mockStdin("invalid\ninvalid\n\n") - if err != nil { - t.Error(errors.ErrorStack(err)) - } - defer cleanUpMockedStdin() - - answer := *GetFromStdin(¶ms) - - if answer != params.DefaultValue { - t.Error("Wrong Answer.\nExpected default answer: " + params.DefaultValue + "\nBut Got: " + answer) - } -} - -func TestGetFromStdin_NoChangeQuestion_NonDefault(t *testing.T) { - - params := GetFromStdinParams{ - Question: "Is this a test?", - DefaultValue: "No", - ValidationRegexPattern: "No|Yes", - } - - err := mockStdin("invalid\nYes\n") - if err != nil { - t.Error(errors.ErrorStack(err)) - } - defer cleanUpMockedStdin() - - answer := *GetFromStdin(¶ms) - - if answer != "Yes" { - t.Error("Wrong Answer.\nExpected: Yes\nBut Got: " + answer) - } -} - -func TestGetFromStdin_ChangeQuestion_DontChange(t *testing.T) { - - params := GetFromStdinParams{ - Question: "Hello?", - DefaultValue: "World", - ValidationRegexPattern: "World|Universe", - InputTerminationString: " ", - } - - err := mockStdin("invalid\nno\n") - if err != nil { - t.Error(errors.ErrorStack(err)) - } - defer cleanUpMockedStdin() - - answer := *AskChangeQuestion(¶ms) - - if answer != "World" { - t.Error("Wrong Answer.\nExpected default: World\nBut Got: " + answer) - } -} - -func TestGetFromStdin_ChangeQuestion_DoChange(t *testing.T) { - - params := GetFromStdinParams{ - Question: "Hello?", - DefaultValue: "World", - ValidationRegexPattern: "World|Universe", - InputTerminationString: "!", - } - - err := mockStdin("invalid\nyes\ninvalid!\nUniverse!\n") - if err != nil { - t.Error(errors.ErrorStack(err)) - } - defer cleanUpMockedStdin() - - answer := *AskChangeQuestion(¶ms) - - if answer != "Universe" { - t.Error("Wrong Answer.\nExpected default: Universe\nBut Got: " + answer) - } -} - -var tmpfile *os.File -var oldStdin *os.File - -func mockStdin(inputString string) error { - //Code from https://stackoverflow.com/a/46365584 (modified) - input := []byte(inputString) - var err error - tmpfile, err = ioutil.TempFile("", "testGetFromStdin") - if err != nil { - return errors.Trace(err) - } - - if _, err := tmpfile.Write(input); err != nil { - return errors.Trace(err) - } - - if _, err := tmpfile.Seek(0, 0); err != nil { - return errors.Trace(err) - } - - oldStdin = os.Stdin - os.Stdin = tmpfile - - return nil -} - -func cleanUpMockedStdin() { - os.Remove(tmpfile.Name()) - os.Stdin = oldStdin -}