github.com/quickfeed/quickfeed@v0.0.0-20240507093252-ed8ca812a09c/assignments/assignments.go (about) 1 package assignments 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "time" 8 9 "github.com/quickfeed/quickfeed/ci" 10 "github.com/quickfeed/quickfeed/database" 11 "github.com/quickfeed/quickfeed/qf" 12 "github.com/quickfeed/quickfeed/scm" 13 "go.uber.org/zap" 14 ) 15 16 // MaxWait is the maximum time allowed for updating a course's assignments 17 // and docker image before aborting. 18 const MaxWait = 5 * time.Minute 19 20 var updateMutex = sync.Mutex{} 21 22 // UpdateFromTestsRepo updates the database record for the course assignments. 23 // 24 // This will be called in response to a push event to the 'tests' repo, which 25 // should happen infrequently. It may also be called manually by a teacher from 26 // the frontend. 27 // 28 // Note that calling this function concurrently is safe, but it may block the 29 // caller for an extended period, since it may involve cloning the tests repository, 30 // scanning the repository for assignments, building the Docker image, updating the 31 // database and synchronizing tasks to issues on the students' group repositories. 32 func UpdateFromTestsRepo(logger *zap.SugaredLogger, runner ci.Runner, db database.Database, sc scm.SCM, course *qf.Course) { 33 updateMutex.Lock() 34 defer updateMutex.Unlock() 35 36 logger.Debugf("Updating %s from '%s' repository", course.GetCode(), qf.TestsRepo) 37 ctx, cancel := context.WithTimeout(context.Background(), MaxWait) 38 defer cancel() 39 40 clonedTestsRepo, err := sc.Clone(ctx, &scm.CloneOptions{ 41 Organization: course.GetScmOrganizationName(), 42 Repository: qf.TestsRepo, 43 DestDir: course.CloneDir(), 44 }) 45 if err != nil { 46 logger.Errorf("Failed to clone '%s' repository: %v", qf.TestsRepo, err) 47 return 48 } 49 logger.Debugf("Successfully cloned tests repository to: %s", clonedTestsRepo) 50 51 // walk the cloned tests repository and extract the assignments and the course's Dockerfile 52 assignments, dockerfile, err := readTestsRepositoryContent(clonedTestsRepo, course.ID) 53 if err != nil { 54 logger.Errorf("Failed to parse assignments from '%s' repository: %v", qf.TestsRepo, err) 55 return 56 } 57 58 if course.UpdateDockerfile(dockerfile) { 59 // Rebuild the Docker image for the course tagged with the course code 60 if err = buildDockerImage(ctx, logger, runner, course); err != nil { 61 logger.Error(err) 62 return 63 } 64 // Update the course's DockerfileDigest in the database 65 if err := db.UpdateCourse(course); err != nil { 66 logger.Errorf("Failed to update Dockerfile for course %s: %v", course.GetCode(), err) 67 return 68 } 69 } 70 71 // Does not store tasks associated with assignments; tasks are handled separately by synchronizeTasksWithIssues below 72 if err = db.UpdateAssignments(assignments); err != nil { 73 for _, assignment := range assignments { 74 logger.Debugf("Failed to update database for: %v", assignment) 75 } 76 logger.Errorf("Failed to update assignments in database: %v", err) 77 return 78 } 79 logger.Debugf("Assignments for %s successfully updated from '%s' repo", course.GetCode(), qf.TestsRepo) 80 81 if err = synchronizeTasksWithIssues(ctx, db, sc, course, assignments); err != nil { 82 logger.Errorf("Failed to create tasks on '%s' repository: %v", qf.TestsRepo, err) 83 return 84 } 85 } 86 87 // buildDockerImage builds the Docker image for the given course. 88 func buildDockerImage(ctx context.Context, logger *zap.SugaredLogger, runner ci.Runner, course *qf.Course) error { 89 logger.Debugf("Building %s's Dockerfile:\n%v", course.GetCode(), course.GetDockerfile()) 90 out, err := runner.Run(ctx, &ci.Job{ 91 Name: course.JobName(), 92 Image: course.DockerImage(), 93 Dockerfile: course.GetDockerfile(), 94 Commands: []string{`echo -n "Hello from Dockerfile"`}, 95 }) 96 logger.Debugf("Build completed: %s", out) 97 if err != nil { 98 return fmt.Errorf("failed to build image from %s's Dockerfile: %s", course.GetCode(), err) 99 } 100 return nil 101 }