graph/step.go (298 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package graph import ( "fmt" "runtime" "strings" "time" "github.com/Azure/acr-builder/pkg/image" "github.com/Azure/acr-builder/pkg/volume" "github.com/Azure/acr-builder/util" "github.com/docker/distribution/reference" "github.com/pkg/errors" ) const ( // ImmediateExecutionToken defines the when dependency to indicate a step should execute immediately. ImmediateExecutionToken = "-" enabled = "enabled" disabled = "disabled" BuildKitEnv = "DOCKER_BUILDKIT=1" ) var ( errMissingID = errors.New("step is missing an ID") errMissingProps = errors.New("step is missing a cmd, build, or push property") errIDContainsSpace = errors.New("step ID cannot contain spaces") errInvalidDeps = errors.New("step cannot contain other IDs in when if the immediate execution token is specified") errInvalidStepType = errors.New("step must only contain a single build, cmd, or push property") errInvalidRetries = errors.New("step must specify retries >= 0") errInvalidRepeat = errors.New("step must specify repeat >= 0") errInvalidCacheValue = errors.New("invalid value for cache property. Valid values are 'enabled', 'disabled'") errInvalidMountsUse = errors.New("invalid use of Mounts. Mounts must have unique container paths and only used for cmd or build steps") ) type chanBool chan bool // MarshalYAML for chan bool's in step. Avoids having Marshall try to render chan bool values as these // cannot be marshalled. Removing this interface causes a crash when marshaling a task. func (c chanBool) MarshalYAML() (interface{}, error) { return "", nil } // UnMarshalYAML for chan bool's in step. Avoids having UnMarshal try to resolve chan bool values as these // cannot be unmarshaled. Removing this interface causes a crash when umarshaling a task. func (c chanBool) UnmarshalYAML(_ func(interface{}) error) error { return nil } // Step is a step in the execution task. type Step struct { ID string `yaml:"id"` Cmd string `yaml:"cmd"` Build string `yaml:"build"` WorkingDirectory string `yaml:"workingDirectory"` EntryPoint string `yaml:"entryPoint"` User string `yaml:"user"` Network string `yaml:"network"` Isolation string `yaml:"isolation"` CPUS string `yaml:"cpus"` Cache string `yaml:"cache"` Mounts []*volume.Mount `yaml:"volumeMounts"` Push []string `yaml:"push"` Envs []string `yaml:"env"` Expose []string `yaml:"expose"` Ports []string `yaml:"ports"` When []string `yaml:"when"` ExitedWith []int `yaml:"exitedWith"` ExitedWithout []int `yaml:"exitedWithout"` Timeout int `yaml:"timeout"` // CmdDownloadRetries specifies how many times a download in a step will be retried CmdDownloadRetries int `yaml:"cmdDownloadRetries"` CmdDownloadRetryDelayInSeconds int `yaml:"cmdDownloadRetryDelay"` StartDelay int `yaml:"startDelay"` RetryDelayInSeconds int `yaml:"retryDelay"` // Retries specifies how many times a Step will be retried if it fails after its initial execution. Retries int `yaml:"retries"` RetryOnErrors []string `yaml:"retryOnErrors"` // Repeat specifies how many times a Step will be repeated after its initial execution. Repeat int `yaml:"repeat"` Keep bool `yaml:"keep"` Detach bool `yaml:"detach"` Privileged bool `yaml:"privileged"` IgnoreErrors bool `yaml:"ignoreErrors"` DisableWorkingDirectoryOverride bool `yaml:"disableWorkingDirectoryOverride"` Pull bool `yaml:"pull"` UsesBuildkit bool StartTime time.Time EndTime time.Time StepStatus StepStatus // CompletedChan can be used to signal to readers // that the step has been processed. CompletedChan chanBool ImageDependencies []*image.Dependencies Tags []string BuildArgs []string DefaultBuildCacheTag string } // Validate validates the step and returns an error if the Step has problems. func (s *Step) Validate() error { if s == nil { return nil } if s.ID == "" { return errMissingID } if s.Retries < 0 { return errInvalidRetries } if s.Repeat < 0 { return errInvalidRepeat } if (s.IsCmdStep() && s.IsPushStep()) || (s.IsCmdStep() && s.IsBuildStep()) || (s.IsBuildStep() && s.IsPushStep()) { return errInvalidStepType } if util.ContainsSpace(s.ID) { return errIDContainsSpace } if !s.IsCmdStep() && !s.IsBuildStep() && !s.IsPushStep() { return errMissingProps } if s.HasMounts() { if !s.IsCmdStep() && !s.IsBuildStep() { return errInvalidMountsUse } valMounts := ValidateMounts(s.Mounts) if valMounts != nil { return valMounts } } for _, dep := range s.When { if dep == ImmediateExecutionToken && len(s.When) > 1 { return errInvalidDeps } if dep == s.ID { return NewSelfReferencedStepError(fmt.Sprintf("Step ID: %v is self-referenced", s.ID)) } } if s.Cache != "" && !strings.EqualFold(s.Cache, enabled) && !strings.EqualFold(s.Cache, disabled) { return errInvalidCacheValue } // check if the build step contains buildkit ENV var if s.IsBuildStep() { s.UsesBuildkit = invokesBuildkit(s.Envs) } return nil } // ValidateMounts checks each mount is well formed and each container file path is unique func ValidateMounts(mounts []*volume.Mount) error { duplicate := make(map[string]struct{}, len(mounts)) for _, m := range mounts { // call m.Validate() for each mount if err := m.Validate(); err != nil { return err } // make sure each container file path provided is unique if _, exists := duplicate[m.MountPath]; exists { return errors.New("mount with duplicate container file path found") } duplicate[m.MountPath] = struct{}{} } return nil } // ValidateMountVolumeNames checks mount name matches a listed volume func (s *Step) ValidateMountVolumeNames(vols []*volume.Volume) error { // for each mount in the step, check to see that there is a matching volume nameMap := make(map[string]struct{}, len(vols)) for _, v := range vols { nameMap[v.Name] = struct{}{} } for _, m := range s.Mounts { if _, exists := nameMap[m.Name]; !exists { return errors.New("mount name, " + m.Name + ", does not correspond to a volume") } } return nil } // Equals determines whether or not two steps are equal. func (s *Step) Equals(t *Step) bool { if s == nil && t == nil { return true } if s == nil || t == nil { return false } return s.ID == t.ID && s.Keep == t.Keep && s.Detach == t.Detach && s.Cmd == t.Cmd && s.Build == t.Build && util.StringSequenceEquals(s.Push, t.Push) && s.WorkingDirectory == t.WorkingDirectory && s.EntryPoint == t.EntryPoint && util.StringSequenceEquals(s.Ports, t.Ports) && util.StringSequenceEquals(s.Expose, t.Expose) && util.StringSequenceEquals(s.Envs, t.Envs) && s.Timeout == t.Timeout && util.StringSequenceEquals(s.When, t.When) && util.IntSequenceEquals(s.ExitedWith, t.ExitedWith) && util.IntSequenceEquals(s.ExitedWithout, t.ExitedWithout) && s.StartDelay == t.StartDelay && s.StartTime == t.StartTime && s.EndTime == t.EndTime && s.StepStatus == t.StepStatus && s.Privileged == t.Privileged && s.User == t.User && s.Network == t.Network && s.Isolation == t.Isolation && s.Cache == t.Cache && s.IgnoreErrors == t.IgnoreErrors && s.Retries == t.Retries && s.RetryDelayInSeconds == t.RetryDelayInSeconds && s.DisableWorkingDirectoryOverride == t.DisableWorkingDirectoryOverride && s.Pull == t.Pull && s.Repeat == t.Repeat } // ShouldExecuteImmediately returns true if the Step should be executed immediately. func (s *Step) ShouldExecuteImmediately() bool { if s == nil { return false } if len(s.When) == 1 && s.When[0] == ImmediateExecutionToken { return true } return false } // HasNoWhen returns true if the Step has no when clause, false otherwise. func (s *Step) HasNoWhen() bool { if s == nil { return true } return len(s.When) == 0 } // HasMounts returns true if the Step has at least 1 mount listed, false otherwise func (s *Step) HasMounts() bool { if s == nil { return false } return len(s.Mounts) > 0 } // IsCmdStep returns true if the Step is a command step, false otherwise. func (s *Step) IsCmdStep() bool { if s == nil { return false } return s.Cmd != "" } // IsBuildStep returns true if the Step is a build step, false otherwise. func (s *Step) IsBuildStep() bool { if s == nil { return false } return s.Build != "" } // IsPushStep returns true if a Step is a push step, false otherwise. func (s *Step) IsPushStep() bool { if s == nil { return false } return len(s.Push) > 0 } // UpdateBuildStepWithDefaults updates a build step with hyperv isolation on Windows. func (s *Step) UpdateBuildStepWithDefaults() { if s.IsBuildStep() && runtime.GOOS == util.WindowsOS && !strings.Contains(s.Build, "--isolation") { s.Build = fmt.Sprintf("--isolation hyperv %s -m 2GB", s.Build) } } // UseBuildCacheForBuildStep indicates if buildx needs to be used. func (s *Step) UseBuildCacheForBuildStep() bool { return s != nil && s.IsBuildStep() && strings.ToLower(s.Cache) == enabled } // GetBuildCacheImageTag returns a default cacheid used to tag buildx images. func GetBuildCacheImageTag(taskName, stepID string) string { return fmt.Sprintf("cache_%s_%s", taskName, stepID) } // GetCmdWithCacheFlags adds buildx cache parameters to the cmd. func (s *Step) GetCmdWithCacheFlags(taskName, registry string) (string, error) { var domain, path, firstTagPath string var err error if strings.ToLower(s.Cache) != enabled { return "", errors.New("cache needs to be set to 'enabled' to use build cache") } if len(s.Tags) == 0 { return s.Build, nil } for idx, tag := range s.Tags { domain, path, err = getDomainPath(tag) if err != nil { return "", errors.Wrap(err, "failed to parse the tag into a domain and path") } if idx == 0 { firstTagPath = path } if domain != "" { break } } if domain == "" { domain = registry path = firstTagPath } s.DefaultBuildCacheTag = GetBuildCacheImageTag(taskName, s.ID) return addBuildCacheOptsToCmd(domain, path, s.DefaultBuildCacheTag, s.Build) } // getDomainPath gets the domain and path for an image repository func getDomainPath(s string) (string, string, error) { repo, err := reference.Parse(s) if err != nil { return "", "", errors.Wrap(err, "failed to parse the image name") } named, ok := repo.(reference.Named) if !ok { return "", "", errors.New("failed to extract the name from registry url") } d, p := reference.SplitHostname(reference.TrimNamed(named)) return d, p, nil } // addBuildCacheOptsToCmd appends the build cache options to the original Build command func addBuildCacheOptsToCmd(domain, path, tag, originalBuildCmd string) (string, error) { named, err := reference.WithName(domain + "/" + path) if err != nil { return "", errors.Wrap(err, "failed to parse reference to be used for cache image") } cacheImage, err := reference.WithTag(named, tag) if err != nil { return "", errors.Wrap(err, "failed to attach cache ID tag to the repo for build cache") } return fmt.Sprintf("--load --cache-to=type=registry,ref=%s,mode=max --cache-from=type=registry,ref=%s %s", cacheImage.String(), cacheImage.String(), originalBuildCmd), nil } func invokesBuildkit(envs []string) bool { for _, env := range envs { if env == BuildKitEnv { return true } } return false }