diff --git a/project-clone/internal/git/setup.go b/project-clone/internal/git/setup.go index f5f1d8052..5ca3d0133 100644 --- a/project-clone/internal/git/setup.go +++ b/project-clone/internal/git/setup.go @@ -47,6 +47,9 @@ func doInitialGitClone(project *dw.Project) error { // Clone into a temp dir and then move set up project to PROJECTS_ROOT to try and make clone atomic in case // project-clone container is terminated tmpClonePath := path.Join(internal.CloneTmpDir, projectslib.GetClonePath(project)) + if err := os.MkdirAll(path.Dir(tmpClonePath), 0755); err != nil { + return fmt.Errorf("failed to create parent directories for temp clone path %s: %w", tmpClonePath, err) + } var cloneErr error for attempt := 0; attempt <= internal.CloneRetries; attempt++ { if attempt > 0 { @@ -120,6 +123,11 @@ func setupRemotesForExistingProject(project *dw.Project) error { } func copyProjectFromTmpDir(project *dw.Project, tmpClonePath string) error { + projectPath := path.Join(internal.ProjectsRoot, projectslib.GetClonePath(project)) + if err := os.MkdirAll(path.Dir(projectPath), 0755); err != nil { + return fmt.Errorf("failed to create parent directories for project path %s: %w", projectPath, err) + } + if project.Attributes.Exists(internal.ProjectSubDir) { // Only want one directory from the project var err error @@ -128,13 +136,11 @@ func copyProjectFromTmpDir(project *dw.Project, tmpClonePath string) error { return fmt.Errorf("failed to process subDir on project: %w", err) } subDirPath := path.Join(tmpClonePath, subDirSubPath) - projectPath := path.Join(internal.ProjectsRoot, projectslib.GetClonePath(project)) log.Printf("Moving subdirectory %s in project %s from temporary directory to %s", subDirSubPath, project.Name, projectPath) if err := os.Rename(subDirPath, projectPath); err != nil { return fmt.Errorf("failed to move subdirectory of cloned project to %s: %w", internal.ProjectsRoot, err) } } else { - projectPath := path.Join(internal.ProjectsRoot, projectslib.GetClonePath(project)) log.Printf("Moving cloned project %s from temporary directory %s to %s", project.Name, tmpClonePath, projectPath) if err := os.Rename(tmpClonePath, projectPath); err != nil { return fmt.Errorf("failed to move cloned project to %s: %w", internal.ProjectsRoot, err) diff --git a/project-clone/internal/zip/setup.go b/project-clone/internal/zip/setup.go index 2b0fba443..92df5bf5a 100644 --- a/project-clone/internal/zip/setup.go +++ b/project-clone/internal/zip/setup.go @@ -52,6 +52,9 @@ func SetupZipProject(project v1alpha2.Project, httpClient *http.Client) error { } tmpProjectsPath := path.Join(internal.CloneTmpDir, clonePath) + if err := os.MkdirAll(path.Dir(tmpProjectsPath), 0755); err != nil { + return fmt.Errorf("failed to create parent directories for temp path %s: %w", tmpProjectsPath, err) + } zipFilePath := path.Join(tmpDir, fmt.Sprintf("%s.zip", clonePath)) log.Printf("Downloading project archive from %s", url) @@ -67,6 +70,9 @@ func SetupZipProject(project v1alpha2.Project, httpClient *http.Client) error { } // Move unzipped project from tmp dir to final destination + if err := os.MkdirAll(path.Dir(projectPath), 0755); err != nil { + return fmt.Errorf("failed to create parent directories for project path %s: %w", projectPath, err) + } log.Printf("Moving extracted project archive to %s", projectPath) if err := os.Rename(tmpProjectsPath, projectPath); err != nil { return fmt.Errorf("failed to move unzipped project to PROJECTS_ROOT: %w", err) diff --git a/project-clone/test/nested-clone-path-test.devworkspace.yaml b/project-clone/test/nested-clone-path-test.devworkspace.yaml new file mode 100644 index 000000000..b4da35401 --- /dev/null +++ b/project-clone/test/nested-clone-path-test.devworkspace.yaml @@ -0,0 +1,121 @@ +apiVersion: workspace.devfile.io/v1alpha2 +kind: DevWorkspace +metadata: + name: nested-clone-path-test + labels: + app.kubernetes.io/name: devworkspace-project-clone-tests + app.kubernetes.io/part-of: devworkspace-operator + annotations: + controller.devfile.io/debug-start: "true" +spec: + started: true + routingClass: 'basic' + template: + attributes: + controller.devfile.io/storage-type: ephemeral + variables: + test_runner_image: quay.io/devfile/project-clone:next # Requires git, bash + main_repo: https://github.com/devfile/devworkspace-operator.git + default_branch_name: main + projects: + - name: backend-api + clonePath: back/api + git: + remotes: + origin: "{{main_repo}}" + - name: backend-worker + clonePath: back/worker + git: + remotes: + origin: "{{main_repo}}" + - name: frontend-app + clonePath: ui/app + git: + remotes: + origin: "{{main_repo}}" + - name: flat-project + git: + remotes: + origin: "{{main_repo}}" + components: + - name: test-project-clone + container: + image: "{{test_runner_image}}" + memoryLimit: 512Mi + mountSources: true + command: + - "/bin/bash" + - "-c" + - | + set -e + + fail() { + echo "[ERROR] $1" + echo "[ERROR] See project-clone logs: " + echo "[ERROR] oc logs -n $DEVWORKSPACE_NAMESPACE deploy/$DEVWORKSPACE_ID -c project-clone" + exit 1 + } + + if [ -f "${PROJECTS_ROOT}/project-clone-errors.log" ]; then + echo "==== BEGIN PROJECT CLONE LOGS ====" + sed 's/^/ /g' "${PROJECTS_ROOT}/project-clone-errors.log" + echo "==== END PROJECT CLONE LOGS ====" + echo -e "\n\n" + fi + + for project_dir in "back/api" "back/worker" "ui/app" "flat-project"; do + if [ ! -d "${PROJECTS_ROOT}/${project_dir}" ]; then + fail "Project at ${project_dir} not cloned successfully" + fi + done + + echo "Testing nested clonePath project: back/api" + cd "${PROJECTS_ROOT}/back/api" + branch_name=$(git rev-parse --abbrev-ref HEAD) + if [ "$branch_name" != "{{default_branch_name}}" ]; then + fail "Project back/api does not have default branch checked out" + fi + remote_url=$(git config remote.origin.url) + if [ "$remote_url" != "{{main_repo}}" ]; then + fail "Remote 'origin' not configured for back/api" + fi + echo "back/api: on $branch_name, remotes configured" + + echo "Testing nested clonePath project: back/worker" + cd "${PROJECTS_ROOT}/back/worker" + branch_name=$(git rev-parse --abbrev-ref HEAD) + if [ "$branch_name" != "{{default_branch_name}}" ]; then + fail "Project back/worker does not have default branch checked out" + fi + remote_url=$(git config remote.origin.url) + if [ "$remote_url" != "{{main_repo}}" ]; then + fail "Remote 'origin' not configured for back/worker" + fi + echo "back/worker: on $branch_name, remotes configured" + + echo "Testing nested clonePath project: ui/app" + cd "${PROJECTS_ROOT}/ui/app" + branch_name=$(git rev-parse --abbrev-ref HEAD) + if [ "$branch_name" != "{{default_branch_name}}" ]; then + fail "Project ui/app does not have default branch checked out" + fi + remote_url=$(git config remote.origin.url) + if [ "$remote_url" != "{{main_repo}}" ]; then + fail "Remote 'origin' not configured for ui/app" + fi + echo "ui/app: on $branch_name, remotes configured" + + echo "Testing flat clonePath project (no nesting)" + cd "${PROJECTS_ROOT}/flat-project" + branch_name=$(git rev-parse --abbrev-ref HEAD) + if [ "$branch_name" != "{{default_branch_name}}" ]; then + fail "Project flat-project does not have default branch checked out" + fi + remote_url=$(git config remote.origin.url) + if [ "$remote_url" != "{{main_repo}}" ]; then + fail "Remote 'origin' not configured for flat-project" + fi + echo "flat-project: on $branch_name, remotes configured" + + echo "[SUCCESS] Test succeeded. Sleeping indefinitely" + tail -f /dev/null