Skip to content

Commit

Permalink
feat: support creating multiple projects non-interactively (#944)
Browse files Browse the repository at this point in the history
Signed-off-by: Toma Puljak <[email protected]>
  • Loading branch information
Tpuljak authored Sep 20, 2024
1 parent b1c8eab commit 77f9ec7
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 44 deletions.
4 changes: 2 additions & 2 deletions docs/daytona_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
Create a workspace

```
daytona create [REPOSITORY_URL] [flags]
daytona create [REPOSITORY_URL | PROJECT_CONFIG_NAME]... [flags]
```

### Options

```
--blank Create a blank project without using existing configurations
--branch string Specify the Git branch to use in the project
--branch strings Specify the Git branches to use in the projects
--builder BuildChoice Specify the builder (currently auto/devcontainer/none)
-c, --code Open the workspace in the IDE after workspace creation
--custom-image string Create the project with the custom image passed as the flag value; Requires setting --custom-image-user flag as well
Expand Down
1 change: 0 additions & 1 deletion docs/daytona_project-config_add.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ daytona project-config add [flags]
### Options

```
--branch string Specify the Git branch to use in the project
--builder BuildChoice Specify the builder (currently auto/devcontainer/none)
--custom-image string Create the project with the custom image passed as the flag value; Requires setting --custom-image-user flag as well
--custom-image-user string Create the project with the custom image user passed as the flag value; Requires setting --custom-image flag as well
Expand Down
5 changes: 3 additions & 2 deletions hack/docs/daytona_create.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
name: daytona create
synopsis: Create a workspace
usage: daytona create [REPOSITORY_URL] [flags]
usage: daytona create [REPOSITORY_URL | PROJECT_CONFIG_NAME]... [flags]
options:
- name: blank
default_value: "false"
usage: Create a blank project without using existing configurations
- name: branch
usage: Specify the Git branch to use in the project
default_value: '[]'
usage: Specify the Git branches to use in the projects
- name: builder
usage: Specify the builder (currently auto/devcontainer/none)
- name: code
Expand Down
2 changes: 0 additions & 2 deletions hack/docs/daytona_project-config_add.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ name: daytona project-config add
synopsis: Add a project config
usage: daytona project-config add [flags]
options:
- name: branch
usage: Specify the Git branch to use in the project
- name: builder
usage: Specify the builder (currently auto/devcontainer/none)
- name: custom-image
Expand Down
1 change: 0 additions & 1 deletion pkg/cmd/projectconfig/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,6 @@ var projectConfigurationFlags = workspace_util.ProjectConfigurationFlags{
Builder: new(views_util.BuildChoice),
CustomImage: new(string),
CustomImageUser: new(string),
Branch: new(string),
DevcontainerPath: new(string),
EnvVars: new([]string),
Manual: new(bool),
Expand Down
110 changes: 80 additions & 30 deletions pkg/cmd/workspace/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,15 @@ import (
)

var CreateCmd = &cobra.Command{
Use: "create [REPOSITORY_URL]",
Use: "create [REPOSITORY_URL | PROJECT_CONFIG_NAME]...",
Short: "Create a workspace",
Args: cobra.RangeArgs(0, 1),
GroupID: util.WORKSPACE_GROUP,
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
var projects []apiclient.CreateProjectDTO
var workspaceName string
var existingWorkspaceNames []string
var existingProjectConfigName *string
var existingProjectConfigNames []string

apiClient, err := apiclient_util.GetApiClient(nil)
if err != nil {
Expand Down Expand Up @@ -82,7 +81,7 @@ var CreateCmd = &cobra.Command{
}

if len(args) == 0 {
err = processPrompting(apiClient, &workspaceName, &projects, existingWorkspaceNames, ctx)
err = processPrompting(ctx, apiClient, &workspaceName, &projects, existingWorkspaceNames)
if err != nil {
if common.IsCtrlCAbort(err) {
return
Expand All @@ -91,7 +90,7 @@ var CreateCmd = &cobra.Command{
}
}
} else {
existingProjectConfigName, err = processCmdArgument(args[0], apiClient, &projects, ctx)
existingProjectConfigNames, err = processCmdArguments(ctx, args, apiClient, &projects)
if err != nil {
log.Fatal(err)
}
Expand Down Expand Up @@ -124,11 +123,14 @@ var CreateCmd = &cobra.Command{
Msg: "Request submitted\n",
}, logs_view.STATIC_INDEX)

if existingProjectConfigName != nil {
for i, projectConfigName := range existingProjectConfigNames {
if projectConfigName == "" {
continue
}
logs_view.DisplayLogEntry(logs.LogEntry{
ProjectName: existingProjectConfigName,
Msg: fmt.Sprintf("Using detected project config '%s'\n", *existingProjectConfigName),
}, logs_view.FIRST_PROJECT_INDEX)
ProjectName: &projects[i].Name,
Msg: fmt.Sprintf("Using detected project config '%s'\n", projectConfigName),
}, i)
}

targetList, res, err := apiClient.TargetAPI.ListTargets(ctx).Execute()
Expand Down Expand Up @@ -233,7 +235,7 @@ var projectConfigurationFlags = workspace_util.ProjectConfigurationFlags{
Builder: new(views_util.BuildChoice),
CustomImage: new(string),
CustomImageUser: new(string),
Branch: new(string),
Branches: new([]string),
DevcontainerPath: new(string),
EnvVars: new([]string),
Manual: new(bool),
Expand All @@ -254,6 +256,7 @@ func init() {
CreateCmd.Flags().BoolVarP(&codeFlag, "code", "c", false, "Open the workspace in the IDE after workspace creation")
CreateCmd.Flags().BoolVar(&multiProjectFlag, "multi-project", false, "Workspace with multiple projects/repos")
CreateCmd.Flags().BoolVarP(&yesFlag, "yes", "y", false, "Automatically confirm any prompts")
CreateCmd.Flags().StringSliceVar(projectConfigurationFlags.Branches, "branch", []string{}, "Specify the Git branches to use in the projects")

workspace_util.AddProjectConfigurationFlags(CreateCmd, projectConfigurationFlags, true)
}
Expand All @@ -275,8 +278,8 @@ func getTarget(targetList []apiclient.ProviderTarget, activeProfileName string)
return target.GetTargetFromPrompt(targetList, activeProfileName, false)
}

func processPrompting(apiClient *apiclient.APIClient, workspaceName *string, projects *[]apiclient.CreateProjectDTO, workspaceNames []string, ctx context.Context) error {
if workspace_util.CheckAnyProjectConfigurationFlagSet(projectConfigurationFlags) {
func processPrompting(ctx context.Context, apiClient *apiclient.APIClient, workspaceName *string, projects *[]apiclient.CreateProjectDTO, workspaceNames []string) error {
if workspace_util.CheckAnyProjectConfigurationFlagSet(projectConfigurationFlags) || (projectConfigurationFlags.Branches != nil && len(*projectConfigurationFlags.Branches) > 0) {
return errors.New("please provide the repository URL in order to set up custom project details through the CLI")
}

Expand Down Expand Up @@ -337,47 +340,81 @@ func processPrompting(apiClient *apiclient.APIClient, workspaceName *string, pro
return nil
}

func processCmdArgument(argument string, apiClient *apiclient.APIClient, projects *[]apiclient.CreateProjectDTO, ctx context.Context) (*string, error) {
func processCmdArguments(ctx context.Context, repoUrls []string, apiClient *apiclient.APIClient, projects *[]apiclient.CreateProjectDTO) ([]string, error) {
if len(repoUrls) == 0 {
return nil, fmt.Errorf("no repository URLs provided")
}

if len(repoUrls) > 1 && workspace_util.CheckAnyProjectConfigurationFlagSet(projectConfigurationFlags) {
return nil, fmt.Errorf("can't set custom project configuration properties for multiple projects")
}

if *projectConfigurationFlags.Builder != "" && *projectConfigurationFlags.Builder != views_util.DEVCONTAINER && *projectConfigurationFlags.DevcontainerPath != "" {
return nil, fmt.Errorf("can't set devcontainer file path if builder is not set to %s", views_util.DEVCONTAINER)
}

var projectConfig *apiclient.ProjectConfig

repoUrl, err := util.GetValidatedUrl(argument)
if err == nil {
// The argument is a Git URL
return processGitURL(repoUrl, apiClient, projects, ctx)
}
existingProjectConfigNames := []string{}

// The argument is not a Git URL - try getting the project config
projectConfig, _, err = apiClient.ProjectConfigAPI.GetProjectConfig(ctx, argument).Execute()
if err != nil {
return nil, fmt.Errorf("failed to parse the URL or fetch the project config for '%s'", argument)
for i, repoUrl := range repoUrls {
var branch *string
if len(*projectConfigurationFlags.Branches) > i {
branch = &(*projectConfigurationFlags.Branches)[i]
}

validatedUrl, err := util.GetValidatedUrl(repoUrl)
if err == nil {
// The argument is a Git URL
existingProjectConfigName, err := processGitURL(ctx, validatedUrl, apiClient, projects, branch)
if err != nil {
return nil, err
}
if existingProjectConfigName != nil {
existingProjectConfigNames = append(existingProjectConfigNames, *existingProjectConfigName)
} else {
existingProjectConfigNames = append(existingProjectConfigNames, "")
}

continue
}

// The argument is not a Git URL - try getting the project config
projectConfig, _, err = apiClient.ProjectConfigAPI.GetProjectConfig(ctx, repoUrl).Execute()
if err != nil {
return nil, fmt.Errorf("failed to parse the URL or fetch the project config for '%s'", repoUrl)
}

existingProjectConfigName, err := workspace_util.AddProjectFromConfig(projectConfig, apiClient, projects, branch)
if err != nil {
return nil, err
}
if existingProjectConfigName != nil {
existingProjectConfigNames = append(existingProjectConfigNames, *existingProjectConfigName)
} else {
existingProjectConfigNames = append(existingProjectConfigNames, "")
}
}

return workspace_util.AddProjectFromConfig(projectConfig, apiClient, projects, *projectConfigurationFlags.Branch)
dedupProjectNames(projects)

return existingProjectConfigNames, nil
}

func processGitURL(repoUrl string, apiClient *apiclient.APIClient, projects *[]apiclient.CreateProjectDTO, ctx context.Context) (*string, error) {
func processGitURL(ctx context.Context, repoUrl string, apiClient *apiclient.APIClient, projects *[]apiclient.CreateProjectDTO, branch *string) (*string, error) {
encodedURLParam := url.QueryEscape(repoUrl)

if !blankFlag {
projectConfig, res, err := apiClient.ProjectConfigAPI.GetDefaultProjectConfig(ctx, encodedURLParam).Execute()
if err == nil {
return workspace_util.AddProjectFromConfig(projectConfig, apiClient, projects, *projectConfigurationFlags.Branch)
return workspace_util.AddProjectFromConfig(projectConfig, apiClient, projects, branch)
}

if res.StatusCode != http.StatusNotFound {
return nil, apiclient_util.HandleErrorResponse(res, err)
}
}

var branch *string
if *projectConfigurationFlags.Branch != "" {
branch = projectConfigurationFlags.Branch
}

repo, res, err := apiClient.GitProviderAPI.GetGitContext(ctx).Repository(apiclient.GetRepositoryContext{
Url: repoUrl,
Branch: branch,
Expand Down Expand Up @@ -439,3 +476,16 @@ func waitForDial(workspace *apiclient.Workspace, activeProfile *config.Profile,
time.Sleep(time.Second)
}
}

func dedupProjectNames(projects *[]apiclient.CreateProjectDTO) {
projectNames := map[string]int{}

for i, project := range *projects {
if _, ok := projectNames[project.Name]; ok {
(*projects)[i].Name = fmt.Sprintf("%s-%d", project.Name, projectNames[project.Name])
projectNames[project.Name]++
} else {
projectNames[project.Name] = 2
}
}
}
7 changes: 5 additions & 2 deletions pkg/cmd/workspace/util/add_from_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import (
"github.com/daytonaio/daytona/pkg/common"
)

func AddProjectFromConfig(projectConfig *apiclient.ProjectConfig, apiClient *apiclient.APIClient, projects *[]apiclient.CreateProjectDTO, branchFlag string) (*string, error) {
chosenBranchName := branchFlag
func AddProjectFromConfig(projectConfig *apiclient.ProjectConfig, apiClient *apiclient.APIClient, projects *[]apiclient.CreateProjectDTO, branchFlag *string) (*string, error) {
chosenBranchName := ""
if branchFlag != nil {
chosenBranchName = *branchFlag
}

if chosenBranchName == "" {
chosenBranch, err := GetBranchFromProjectConfig(projectConfig, apiClient, 0)
Expand Down
7 changes: 6 additions & 1 deletion pkg/cmd/workspace/util/branch_wizard.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,12 @@ func SetBranchFromWizard(config BranchWizardConfig) (*apiclient.GitRepository, e
}

var branch *apiclient.GitBranch
parentIdentifier := fmt.Sprintf("%s/%s/%s", config.ProviderId, config.Namespace, config.ChosenRepo.Name)
namespace := config.Namespace
if namespace == "" {
namespace = config.ChosenRepo.Owner
}

parentIdentifier := fmt.Sprintf("%s/%s/%s", config.ProviderId, namespace, config.ChosenRepo.Name)
if len(prList) == 0 {
branch = selection.GetBranchFromPrompt(branchList, config.ProjectOrder, parentIdentifier)
if branch == nil {
Expand Down
5 changes: 2 additions & 3 deletions pkg/cmd/workspace/util/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type ProjectConfigurationFlags struct {
Builder *views_util.BuildChoice
CustomImage *string
CustomImageUser *string
Branch *string
Branches *[]string
DevcontainerPath *string
EnvVars *[]string
Manual *bool
Expand All @@ -25,7 +25,6 @@ type ProjectConfigurationFlags struct {
func AddProjectConfigurationFlags(cmd *cobra.Command, flags ProjectConfigurationFlags, multiProjectFlagException bool) {
cmd.Flags().StringVar(flags.CustomImage, "custom-image", "", "Create the project with the custom image passed as the flag value; Requires setting --custom-image-user flag as well")
cmd.Flags().StringVar(flags.CustomImageUser, "custom-image-user", "", "Create the project with the custom image user passed as the flag value; Requires setting --custom-image flag as well")
cmd.Flags().StringVar(flags.Branch, "branch", "", "Specify the Git branch to use in the project")
cmd.Flags().StringVar(flags.DevcontainerPath, "devcontainer-path", "", "Automatically assign the devcontainer builder with the path passed as the flag value")
cmd.Flags().Var(flags.Builder, "builder", fmt.Sprintf("Specify the builder (currently %s/%s/%s)", views_util.AUTOMATIC, views_util.DEVCONTAINER, views_util.NONE))
cmd.Flags().StringArrayVar(flags.EnvVars, "env", []string{}, "Specify environment variables (e.g. --env 'KEY1=VALUE1' --env 'KEY2=VALUE2' ...')")
Expand All @@ -47,7 +46,7 @@ func AddProjectConfigurationFlags(cmd *cobra.Command, flags ProjectConfiguration
}

func CheckAnyProjectConfigurationFlagSet(flags ProjectConfigurationFlags) bool {
return *flags.CustomImage != "" || *flags.CustomImageUser != "" || *flags.Branch != "" || *flags.DevcontainerPath != "" || *flags.Builder != "" || len(*flags.EnvVars) > 0
return *flags.CustomImage != "" || *flags.CustomImageUser != "" || *flags.DevcontainerPath != "" || *flags.Builder != "" || len(*flags.EnvVars) > 0
}

func IsProjectRunning(workspace *apiclient.WorkspaceDTO, projectName string) bool {
Expand Down

0 comments on commit 77f9ec7

Please sign in to comment.