diff --git a/controllers/workspace/devworkspace_controller.go b/controllers/workspace/devworkspace_controller.go index d6c100876..b019aef28 100644 --- a/controllers/workspace/devworkspace_controller.go +++ b/controllers/workspace/devworkspace_controller.go @@ -323,6 +323,10 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request return r.failWorkspace(workspace, fmt.Sprintf("Failed to process workspace environment variables: %s", err), metrics.ReasonBadRequest, reqLogger, &reconcileStatus), nil } + // Validate that projects, dependentProjects, and starterProjects do not collide + if err := projects.ValidateAllProjects(&workspace.Spec.Template); err != nil { + return r.failWorkspace(workspace, fmt.Sprintf("Invalid devfile: %s", err), metrics.ReasonBadRequest, reqLogger, &reconcileStatus), nil + } // Add init container to clone projects projectCloneOptions := projects.Options{ Image: workspace.Config.Workspace.ProjectCloneConfig.Image, diff --git a/pkg/library/annotate/plugins.go b/pkg/library/annotate/plugins.go index c0d1bd4ac..5264cb903 100644 --- a/pkg/library/annotate/plugins.go +++ b/pkg/library/annotate/plugins.go @@ -49,4 +49,10 @@ func AddSourceAttributesForTemplate(sourceID string, template *dw.DevWorkspaceTe } template.StarterProjects[idx].Attributes.PutString(constants.PluginSourceAttribute, sourceID) } + for idx, project := range template.DependentProjects { + if project.Attributes == nil { + template.DependentProjects[idx].Attributes = attributes.Attributes{} + } + template.DependentProjects[idx].Attributes.PutString(constants.PluginSourceAttribute, sourceID) + } } diff --git a/pkg/library/container/mountSources.go b/pkg/library/container/mountSources.go index 11386ad4d..9de045eea 100644 --- a/pkg/library/container/mountSources.go +++ b/pkg/library/container/mountSources.go @@ -102,7 +102,9 @@ func handleMountSources(k8sContainer *corev1.Container, devfileContainer *dw.Con // // 2. If the workspace has a starter project that is selected, its name will be used as the project source path. // -// 3. Otherwise, the returned project source path will be an empty string. +// 3. If the workspace has any dependentProjects, the first one will be selected. +// +// 4. Otherwise, the returned project source path will be an empty string. func getProjectSourcePath(workspace *dw.DevWorkspaceTemplateSpec) (string, error) { projects := workspace.Projects // If there are any projects, return the first one's clone path @@ -118,6 +120,12 @@ func getProjectSourcePath(workspace *dw.DevWorkspaceTemplateSpec) (string, error // Starter projects do not allow specifying a clone path, so use the name return selectedStarterProject.Name, nil } + + // Finally, check if there are any dependent projects + if len(workspace.DependentProjects) > 0 { + return projectslib.GetClonePath(&workspace.DependentProjects[0]), nil + } + return "", nil } diff --git a/pkg/library/defaults/helper.go b/pkg/library/defaults/helper.go index 543b97cab..6c80791e5 100644 --- a/pkg/library/defaults/helper.go +++ b/pkg/library/defaults/helper.go @@ -28,8 +28,10 @@ func ApplyDefaultTemplate(workspace *common.DevWorkspaceWithConfig) { } defaultCopy := workspace.Config.Workspace.DefaultTemplate.DeepCopy() originalProjects := workspace.Spec.Template.Projects + originalDependentProjects := workspace.Spec.Template.DependentProjects workspace.Spec.Template.DevWorkspaceTemplateSpecContent = *defaultCopy workspace.Spec.Template.Projects = append(workspace.Spec.Template.Projects, originalProjects...) + workspace.Spec.Template.DependentProjects = append(workspace.Spec.Template.DependentProjects, originalDependentProjects...) } func NeedsDefaultTemplate(workspace *common.DevWorkspaceWithConfig) bool { diff --git a/pkg/library/projects/clone.go b/pkg/library/projects/clone.go index ab3b9c35b..6efbdd4be 100644 --- a/pkg/library/projects/clone.go +++ b/pkg/library/projects/clone.go @@ -18,6 +18,7 @@ package projects import ( "fmt" + "strings" dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" @@ -41,12 +42,56 @@ type Options struct { Env []corev1.EnvVar } +// ValidateAllProjectsvalidates that no two projects, dependentProjects or starterProjects (if one is selected) share +// the same name or cloned path +func ValidateAllProjects(workspace *dw.DevWorkspaceTemplateSpec) error { + // Map of project names to project sources (project, dependentProject, starterProject) + projectNames := map[string][]string{} + // Map of project clone paths to project sources ("project ", "starterProject ", "dependentProject ") + clonePaths := map[string][]string{} + + for idx, project := range workspace.Projects { + projectNames[project.Name] = append(projectNames[project.Name], "projects") + clonePath := GetClonePath(&workspace.Projects[idx]) + clonePaths[clonePath] = append(clonePaths[clonePath], fmt.Sprintf("project %s", project.Name)) + } + + for idx, project := range workspace.DependentProjects { + projectNames[project.Name] = append(projectNames[project.Name], "dependentProjects") + clonePath := GetClonePath(&workspace.DependentProjects[idx]) + clonePaths[clonePath] = append(clonePaths[clonePath], fmt.Sprintf("dependentProject %s", project.Name)) + } + + starterProject, err := GetStarterProject(workspace) + if err != nil { + return err + } + if starterProject != nil { + projectNames[starterProject.Name] = append(projectNames[starterProject.Name], "starterProjects") + // Starter projects do not have a clonePath field + clonePaths[starterProject.Name] = append(clonePaths[starterProject.Name], fmt.Sprintf("starterProject %s", starterProject.Name)) + } + + for projectName, projectTypes := range projectNames { + if len(projectTypes) > 1 { + return fmt.Errorf("found multiple projects with the same name '%s' in: %s", projectName, strings.Join(projectTypes, ", ")) + } + } + for clonePath, projects := range clonePaths { + if len(projects) > 1 { + return fmt.Errorf("found multiple projects with the same clone path (%s): %s", clonePath, strings.Join(projects, ", ")) + } + } + + return nil +} + func GetProjectCloneInitContainer(workspace *dw.DevWorkspaceTemplateSpec, options Options, proxyConfig *controllerv1alpha1.Proxy) (*corev1.Container, error) { starterProject, err := GetStarterProject(workspace) if err != nil { return nil, err } - if len(workspace.Projects) == 0 && starterProject == nil { + if len(workspace.Projects) == 0 && len(workspace.DependentProjects) == 0 && starterProject == nil { return nil, nil } if workspace.Attributes.GetString(constants.ProjectCloneAttribute, nil) == constants.ProjectCloneDisable { diff --git a/project-clone/main.go b/project-clone/main.go index f74f72045..c5691661e 100644 --- a/project-clone/main.go +++ b/project-clone/main.go @@ -65,6 +65,7 @@ func main() { } projects := workspace.Projects + projects = append(projects, workspace.DependentProjects...) starterProject, err := projectslib.GetStarterProject(workspace) if err != nil { diff --git a/webhook/workspace/handler/validate.go b/webhook/workspace/handler/validate.go index 2e86e6578..f6e7093ec 100644 --- a/webhook/workspace/handler/validate.go +++ b/webhook/workspace/handler/validate.go @@ -40,6 +40,7 @@ func (h *WebhookHandler) ValidateDevfile(ctx context.Context, req admission.Requ events := workspace.Events projects := workspace.Projects starterProjects := workspace.StarterProjects + dependentProjects := workspace.DependentProjects var devfileErrors []string @@ -59,6 +60,13 @@ func (h *WebhookHandler) ValidateDevfile(ctx context.Context, req admission.Requ } } + if dependentProjects != nil { + dependentProjectsErrors := devfilevalidation.ValidateProjects(dependentProjects) + if dependentProjectsErrors != nil { + devfileErrors = append(devfileErrors, dependentProjectsErrors.Error()) + } + } + // validate starter projects if starterProjects != nil { starterProjectErrors := devfilevalidation.ValidateStarterProjects(starterProjects)