mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-05 14:55:44 +00:00
The intermittent Windows test crashes came from step errors formatting a nil `rc.Run`. `fmt` recovers the nil dereference, but on CPUs with AMX the recovered fault overflows into the heap (https://github.com/golang/go/issues/81238). The errors now name the job, and the Windows test VM hides AMX and runs on UTC, so cached boots no longer jump back 8 hours mid-test. Windows commands now start suspended and resume once they are in their job objects, so no child escapes them. That drops the process scan reading other processes' memory and the last `unsafe` code, which `depguard` now denies. Step processes outlive a crashed runner, and commands are no longer looked up in the runner's working directory, both as with GitHub's runner. Like GitHub's runner, host jobs on Linux and macOS now end the processes carrying the job's `RUNNER_TRACKING_ID`, and image pulls and Dockerfile action builds are retried. Reviewed-on: https://gitea.com/gitea/runner/pulls/1376 Reviewed-by: bircni <[email protected]> Co-authored-by: silverwind <[email protected]>
149 lines
4.5 KiB
Go
149 lines
4.5 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// Copyright 2020 The nektos/act Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd || freebsd))
|
|
|
|
package container
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gitea.com/gitea/runner/act/common"
|
|
|
|
"github.com/distribution/reference"
|
|
"github.com/moby/moby/api/pkg/authconfig"
|
|
"github.com/moby/moby/api/types/registry"
|
|
"github.com/moby/moby/client"
|
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
|
)
|
|
|
|
// NewDockerPullExecutor function to create a run executor for the container
|
|
func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
|
|
return common.Executor(func(ctx context.Context) error {
|
|
logger := common.Logger(ctx)
|
|
logger.Debugf("docker pull %v", input.Image)
|
|
|
|
if common.Dryrun(ctx) {
|
|
return nil
|
|
}
|
|
|
|
// skip the pull when the image is already here: either none was forced, or a digest
|
|
// pins the content so a forced pull could only fetch the same bytes again
|
|
if !input.ForcePull || isPinnedImage(input.Image) {
|
|
imageExists, err := ImageExistsLocally(ctx, input.Image, input.Platform)
|
|
logger.Debugf("Image exists? %v", imageExists)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to determine if image already exists for image '%s' (%s): %w", input.Image, input.Platform, err)
|
|
}
|
|
if imageExists {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
imageRef := cleanImage(ctx, input.Image)
|
|
logger.Debugf("pulling image '%v' (%s)", imageRef, input.Platform)
|
|
|
|
cli, err := GetDockerClient(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
imagePullOptions, err := getImagePullOptions(ctx, input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// the daemon reports a failure that happens after the first progress line in the
|
|
// stream rather than on the call itself, so both have to be checked
|
|
pullOnce := func(opts client.ImagePullOptions) error {
|
|
reader, err := cli.ImagePull(ctx, imageRef, opts)
|
|
streamErr := logDockerResponse(logger, reader, err != nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return streamErr
|
|
}
|
|
|
|
err = pullOnce(imagePullOptions)
|
|
if err != nil && imagePullOptions.RegistryAuth != "" && strings.Contains(err.Error(), "unauthorized") {
|
|
logger.Errorf("pulling image '%v' (%s) failed with credentials %s retrying without them, please check for stale docker config files", imageRef, input.Platform, err.Error())
|
|
imagePullOptions.RegistryAuth = ""
|
|
err = pullOnce(imagePullOptions)
|
|
}
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
// a registry that is down should not fail a job whose image is already here
|
|
if exists, existsErr := ImageExistsLocally(ctx, input.Image, input.Platform); existsErr == nil && exists {
|
|
logger.Warnf("could not update image '%s' (%s), continuing with the local copy: %v", imageRef, input.Platform, err)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to pull image '%s' (%s): %w", imageRef, input.Platform, err)
|
|
}).Retry(3)
|
|
}
|
|
|
|
func getImagePullOptions(ctx context.Context, input NewDockerPullExecutorInput) (client.ImagePullOptions, error) {
|
|
imagePullOptions := client.ImagePullOptions{}
|
|
platform, err := parsePlatform(input.Platform)
|
|
if err != nil {
|
|
return imagePullOptions, err
|
|
}
|
|
if platform != nil {
|
|
imagePullOptions.Platforms = []specs.Platform{*platform}
|
|
}
|
|
logger := common.Logger(ctx)
|
|
|
|
if input.Username != "" && input.Password != "" {
|
|
logger.Debugf("using authentication for docker pull")
|
|
|
|
encodedAuth, err := authconfig.Encode(registry.AuthConfig{
|
|
Username: input.Username,
|
|
Password: input.Password,
|
|
})
|
|
if err != nil {
|
|
return imagePullOptions, err
|
|
}
|
|
|
|
imagePullOptions.RegistryAuth = encodedAuth
|
|
} else {
|
|
authConfig, err := LoadDockerAuthConfig(ctx, input.Image)
|
|
if err != nil {
|
|
return imagePullOptions, err
|
|
}
|
|
if authConfig.Username == "" && authConfig.Password == "" && authConfig.IdentityToken == "" && authConfig.RegistryToken == "" {
|
|
return imagePullOptions, nil
|
|
}
|
|
logger.Info("using DockerAuthConfig authentication for docker pull")
|
|
|
|
imagePullOptions.RegistryAuth, err = authconfig.Encode(authConfig)
|
|
if err != nil {
|
|
return imagePullOptions, err
|
|
}
|
|
}
|
|
|
|
return imagePullOptions, nil
|
|
}
|
|
|
|
func isPinnedImage(image string) bool {
|
|
ref, err := reference.ParseAnyReference(image)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_, pinned := ref.(reference.Canonical)
|
|
return pinned
|
|
}
|
|
|
|
func cleanImage(ctx context.Context, imageName string) string {
|
|
ref, err := reference.ParseAnyReference(imageName)
|
|
if err != nil {
|
|
common.Logger(ctx).Error(err)
|
|
return ""
|
|
}
|
|
|
|
return ref.String()
|
|
}
|