vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Let a job start service containers beside it
c1929d2fceb5e9e65204d5eef907d0781b9a5391 · Keenan Nemetz · 5d ago · parent 1d06b008
ci/agent/agent.go +65−11
| @@ -107,6 +107,10 @@ type runnerJob struct { | |||
| 107 | 107 | ImageSHA string `json:"image_sha,omitempty"` | |
| 108 | 108 | // Env is the job's declared environment, pinned when the run was queued. | |
| 109 | 109 | Env map[string]string `json:"env,omitempty"` | |
| 110 | // Services are the containers started beside the job, in declaration order. A job | ||
| 111 | // with services asked for network: bridge, and the runner gives the job and its | ||
| 112 | // services one network for this attempt. | ||
| 113 | Services []runnerService `json:"services,omitempty"` | ||
| 110 | 114 | // Secrets are the declared secrets this repository has, and Missing are the | |
| 111 | 115 | // names it declared that it does not. A missing one leaves its variable unset and | |
| 112 | 116 | // gets a line in the log; it is not a failure, because a workflow is allowed an | |
| @@ -131,6 +135,13 @@ type runnerCredential struct { | |||
| 131 | 135 | Fingerprint string `json:"fingerprint"` | |
| 132 | 136 | } | |
| 133 | 137 | ||
| 138 | // runnerService is one container the job asked to run beside it. | ||
| 139 | type runnerService struct { | ||
| 140 | ID string `json:"id"` | ||
| 141 | Image string `json:"image"` | ||
| 142 | Env map[string]string `json:"env,omitempty"` | ||
| 143 | } | ||
| 144 | |||
| 134 | 145 | // runnerSecret is one delivered value and the version it came from. The version is | |
| 135 | 146 | // what the attempt record reports, so a result says which value ran without | |
| 136 | 147 | // carrying it. | |
| @@ -288,6 +299,7 @@ func Run(ctx context.Context, opts Options) error { | |||
| 288 | 299 | } | |
| 289 | 300 | cleanupJobTrees(cache) | |
| 290 | 301 | cleanupStaleContainers(box) | |
| 302 | cleanupStaleNetworks(box) | ||
| 291 | 303 | // Bound the saved checkouts before work starts, the same way the job cache is | |
| 292 | 304 | // bound below: a runner whose disk filled up while it was not watching should | |
| 293 | 305 | // not start by failing a job. | |
| @@ -601,7 +613,10 @@ func (s *runnerSession) run(remote, cache string, job *runnerJob, lease time.Dur | |||
| 601 | 613 | s.finish(remote, co, job, log, 1, nil, nil, nil, box) | |
| 602 | 614 | return | |
| 603 | 615 | } | |
| 616 | jobBox := &jobSandbox{} | ||
| 617 | defer jobBox.Stop() | ||
| 604 | 618 | fail := func(format string, args ...any) { | |
| 619 | jobBox.StopWithLogs(log) | ||
| 605 | 620 | msg := mask.String(fmt.Sprintf(format, args...)) | |
| 606 | 621 | fmt.Fprintf(os.Stderr, "vole runner: %s\n", msg) | |
| 607 | 622 | fmt.Fprintf(log, "\n%s\n", msg) | |
| @@ -650,6 +665,52 @@ func (s *runnerSession) run(remote, cache string, job *runnerJob, lease time.Dur | |||
| 650 | 665 | fail("%s", err) | |
| 651 | 666 | return | |
| 652 | 667 | } | |
| 668 | // Services are containers beside the job on a network of their own. They start | ||
| 669 | // after the image is resolved so a slow build does not leave a database running, | ||
| 670 | // and before the job so their names resolve for the first step. | ||
| 671 | if len(job.Services) > 0 { | ||
| 672 | netName := serviceNetworkName(box, job) | ||
| 673 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||
| 674 | net, err := container.CreateNetwork(ctx, box.Config, netName, box.labels(job)) | ||
| 675 | cancel() | ||
| 676 | if err != nil { | ||
| 677 | fail("%s", err) | ||
| 678 | return | ||
| 679 | } | ||
| 680 | jobBox.network = net | ||
| 681 | serviceConfig := boxConfig | ||
| 682 | serviceConfig.Network = netName | ||
| 683 | boxConfig.Network = netName | ||
| 684 | ids := make([]string, 0, len(job.Services)) | ||
| 685 | for _, svc := range job.Services { | ||
| 686 | c, err := container.StartService(serviceConfig, container.ServiceSpec{ | ||
| 687 | Name: serviceContainerName(box, job, svc.ID), | ||
| 688 | Alias: svc.ID, | ||
| 689 | Image: svc.Image, | ||
| 690 | Env: svc.Env, | ||
| 691 | Labels: box.labels(job), | ||
| 692 | }) | ||
| 693 | if err != nil { | ||
| 694 | fail("%s", err) | ||
| 695 | return | ||
| 696 | } | ||
| 697 | jobBox.services = append(jobBox.services, c) | ||
| 698 | jobBox.serviceIDs = append(jobBox.serviceIDs, svc.ID) | ||
| 699 | ids = append(ids, svc.ID+" ("+svc.Image+")") | ||
| 700 | } | ||
| 701 | fmt.Fprintf(log, "services: %s\n", strings.Join(ids, ", ")) | ||
| 702 | for i, c := range jobBox.services { | ||
| 703 | running, err := c.Running(context.Background()) | ||
| 704 | if err != nil { | ||
| 705 | fail("service %s: %s", job.Services[i].ID, err) | ||
| 706 | return | ||
| 707 | } | ||
| 708 | if !running { | ||
| 709 | fail("service %s exited before the steps started", job.Services[i].ID) | ||
| 710 | return | ||
| 711 | } | ||
| 712 | } | ||
| 713 | } | ||
| 653 | 714 | cacheMounts := s.cacheFor(job) | |
| 654 | 715 | mounts := cacheMounts | |
| 655 | 716 | // The run directory is mounted at /run/vole for the pinned event payload and for | |
| @@ -744,11 +805,7 @@ func (s *runnerSession) run(remote, cache string, job *runnerJob, lease time.Dur | |||
| 744 | 805 | fail("%s", err) | |
| 745 | 806 | return | |
| 746 | 807 | } | |
| 747 | defer func() { | ||
| 748 | if sandbox != nil { | ||
| 749 | sandbox.Stop() | ||
| 750 | } | ||
| 751 | }() | ||
| 808 | jobBox.job = sandbox | ||
| 752 | 809 | ||
| 753 | 810 | steps := make([]runs.StepResult, 0, len(job.Steps)) | |
| 754 | 811 | // The job's budget starts with its first step. An image build has its own cap, so a | |
| @@ -790,8 +847,7 @@ func (s *runnerSession) run(remote, cache string, job *runnerJob, lease time.Dur | |||
| 790 | 847 | // already wrote the line to the log. Keep the partial attempt: the log and | |
| 791 | 848 | // the timings are what a reader asks for first after "what had it done". | |
| 792 | 849 | steps = append(steps, result) | |
| 793 | sandbox.Stop() | ||
| 794 | sandbox = nil | ||
| 850 | jobBox.StopWithLogs(log) | ||
| 795 | 851 | s.finish(remote, co, job, log, code, steps, nil, nil, box) | |
| 796 | 852 | return | |
| 797 | 853 | } | |
| @@ -800,8 +856,7 @@ func (s *runnerSession) run(remote, cache string, job *runnerJob, lease time.Dur | |||
| 800 | 856 | result.LogSize = log.Len() - logStart | |
| 801 | 857 | result.Finished = time.Now().UTC() | |
| 802 | 858 | steps = append(steps, result) | |
| 803 | sandbox.Stop() | ||
| 804 | sandbox = nil | ||
| 859 | jobBox.StopWithLogs(log) | ||
| 805 | 860 | s.finish(remote, co, job, log, code, steps, nil, nil, box) | |
| 806 | 861 | return | |
| 807 | 862 | } | |
| @@ -810,8 +865,7 @@ func (s *runnerSession) run(remote, cache string, job *runnerJob, lease time.Dur | |||
| 810 | 865 | // The steps are done. Stop the container before reading artifacts or deploying a | |
| 811 | 866 | // site, so a process the workflow left in the background cannot change the files | |
| 812 | 867 | // while the runner is publishing them. | |
| 813 | sandbox.Stop() | ||
| 814 | sandbox = nil | ||
| 868 | jobBox.StopJob() | ||
| 815 | 869 | arts, releaseArts, err := publishArtifacts(remote, co.Work, job, log) | |
| 816 | 870 | if err != nil { | |
| 817 | 871 | fail("%s", err) | |