Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support creating multiple projects non-interactively #944

Merged
merged 4 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading