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  }