package api
import "time"
// RecordStageAndStepInfo records details about each build stage and step
func RecordStageAndStepInfo(stages []StageInfo, stageName StageName, stepName StepName, startTime time.Time, endTime time.Time) []StageInfo {
// Make sure that the stages slice is initialized
if len(stages) == 0 {
stages = make([]StageInfo, 0)
}
// If the stage already exists update the endTime and Duration, and append the new step.
for stageKey, stageVal := range stages {
if stageVal.Name == stageName {
stages[stageKey].DurationMilliseconds = endTime.Sub(stages[stageKey].StartTime).Nanoseconds() / int64(time.Millisecond)
if len(stages[stageKey].Steps) == 0 {
stages[stageKey].Steps = make([]StepInfo, 0)
}
stages[stageKey].Steps = append(stages[stageKey].Steps, StepInfo{
Name: stepName,
StartTime: startTime,
DurationMilliseconds: endTime.Sub(startTime).Nanoseconds() / int64(time.Millisecond),
})
return stages
}
}
// If the stageName does not exist, add it to the slice along with the new step.
steps := make([]StepInfo, 0)
steps = append(steps, StepInfo{
Name: stepName,
StartTime: startTime,
DurationMilliseconds: endTime.Sub(startTime).Nanoseconds() / int64(time.Millisecond),
})
stages = append(stages, StageInfo{
Name: stageName,
StartTime: startTime,
DurationMilliseconds: endTime.Sub(startTime).Nanoseconds() / int64(time.Millisecond),
Steps: steps,
})
return stages
}
package api
import (
"errors"
"fmt"
"net/url"
"path/filepath"
"strings"
"time"
"github.com/openshift/source-to-image/pkg/scm/git"
utilglog "github.com/openshift/source-to-image/pkg/util/glog"
"github.com/openshift/source-to-image/pkg/util/user"
)
var glog = utilglog.StderrLog
// invalidFilenameCharacters contains a list of character we consider malicious
// when injecting the directories into containers.
const invalidFilenameCharacters = `;*?"<>|%#$!+{}&[],"'` + "`"
const (
// PullAlways means that we always attempt to pull the latest image.
PullAlways PullPolicy = "always"
// PullNever means that we never pull an image, but only use a local image.
PullNever PullPolicy = "never"
// PullIfNotPresent means that we pull if the image isn't present on disk.
PullIfNotPresent PullPolicy = "if-not-present"
// DefaultBuilderPullPolicy specifies the default pull policy to use
DefaultBuilderPullPolicy = PullIfNotPresent
// DefaultRuntimeImagePullPolicy specifies the default pull policy to use.
DefaultRuntimeImagePullPolicy = PullIfNotPresent
// DefaultPreviousImagePullPolicy specifies policy for pulling the previously
// build Docker image when doing incremental build
DefaultPreviousImagePullPolicy = PullIfNotPresent
)
// Config contains essential fields for performing build.
type Config struct {
// DisplayName is a result image display-name label. This defaults to the
// output image name.
DisplayName string
// Description is a result image description label. The default is no
// description.
Description string
// BuilderImage describes which image is used for building the result images.
BuilderImage string
// BuilderImageVersion provides optional version information about the builder image.
BuilderImageVersion string
// BuilderBaseImageVersion provides optional version information about the builder base image.
BuilderBaseImageVersion string
// RuntimeImage specifies the image that will be a base for resulting image
// and will be used for running an application. By default, BuilderImage is
// used for building and running, but the latter may be overridden.
RuntimeImage string
// RuntimeImagePullPolicy specifies when to pull a runtime image.
RuntimeImagePullPolicy PullPolicy
// RuntimeAuthentication holds the authentication information for pulling the
// runtime Docker images from private repositories.
RuntimeAuthentication AuthConfig
// RuntimeArtifacts specifies a list of source/destination pairs that will
// be copied from builder to a runtime image. Source can be a file or
// directory. Destination must be a directory. Regardless whether it
// is an absolute or relative path, it will be placed into image's WORKDIR.
// Destination also can be empty or equals to ".", in this case it just
// refers to a root of WORKDIR.
// In case it's empty, S2I will try to get this list from
// io.openshift.s2i.assemble-input-files label on a RuntimeImage.
RuntimeArtifacts VolumeList
// DockerConfig describes how to access host docker daemon.
DockerConfig *DockerConfig
// DockerCfgPath provides the path to the .dockercfg file
DockerCfgPath string
// PullAuthentication holds the authentication information for pulling the
// Docker images from private repositories
PullAuthentication AuthConfig
// IncrementalAuthentication holds the authentication information for pulling the
// previous image from private repositories
IncrementalAuthentication AuthConfig
// DockerNetworkMode is used to set the docker network setting to --net=container:<id>
// when the builder is invoked from a container.
DockerNetworkMode DockerNetworkMode
// PreserveWorkingDir describes if working directory should be left after processing.
PreserveWorkingDir bool
// IgnoreSubmodules determines whether we will attempt to pull in submodules
// (via --recursive or submodule init)
IgnoreSubmodules bool
// Source URL describing the location of sources used to build the result image.
Source *git.URL
// Tag is a result image tag name.
Tag string
// BuilderPullPolicy specifies when to pull the builder image
BuilderPullPolicy PullPolicy
// PreviousImagePullPolicy specifies when to pull the previously build image
// when doing incremental build
PreviousImagePullPolicy PullPolicy
// Incremental describes whether to try to perform incremental build.
Incremental bool
// IncrementalFromTag sets an alternative image tag to look for existing
// artifacts. Tag is used by default if this is not set.
IncrementalFromTag string
// RemovePreviousImage describes if previous image should be removed after successful build.
// This applies only to incremental builds.
RemovePreviousImage bool
// Environment is a map of environment variables to be passed to the image.
Environment EnvironmentList
// EnvironmentFile provides the path to a file with list of environment
// variables.
EnvironmentFile string
// LabelNamespace provides the namespace under which the labels will be generated.
LabelNamespace string
// CallbackURL is a URL which is called upon successful build to inform about that fact.
CallbackURL string
// ScriptsURL is a URL describing where to fetch the S2I scripts from during build process.
// This url can be a reference within the builder image if the scheme is specified as image://
ScriptsURL string
// Destination specifies a location where the untar operation will place its artifacts.
Destination string
// WorkingDir describes temporary directory used for downloading sources, scripts and tar operations.
WorkingDir string
// WorkingSourceDir describes the subdirectory off of WorkingDir set up during the repo download
// that is later used as the root for ignore processing
WorkingSourceDir string
// LayeredBuild describes if this is build which layered scripts and sources on top of BuilderImage.
LayeredBuild bool
// Operate quietly. Progress and assemble script output are not reported, only fatal errors.
// (default: false).
Quiet bool
// ForceCopy results in only the file SCM plugin being used (i.e. no `git clone`); allows for empty directories to be included
// in resulting image (since git does not support that).
// (default: false).
ForceCopy bool
// Specify a relative directory inside the application repository that should
// be used as a root directory for the application.
ContextDir string
// AllowedUIDs is a list of user ranges of users allowed to run the builder image.
// If a range is specified and the builder (or runtime) image uses a non-numeric
// user or a user that is outside the specified range, then the build fails.
AllowedUIDs user.RangeList
// AssembleUser specifies the user to run the assemble script in container
AssembleUser string
// RunImage will trigger a "docker run ..." invocation of the produced image so the user
// can see if it operates as he would expect
RunImage bool
// Usage allows for properly shortcircuiting s2i logic when `s2i usage` is invoked
Usage bool
// Injections specifies a list source/destination folders that are injected to
// the container that runs assemble.
// All files we inject will be truncated after the assemble script finishes.
Injections VolumeList
// CGroupLimits describes the cgroups limits that will be applied to any containers
// run by s2i.
CGroupLimits *CGroupLimits
// DropCapabilities contains a list of capabilities to drop when executing containers
DropCapabilities []string
// ScriptDownloadProxyConfig optionally specifies the http and https proxy
// to use when downloading scripts
ScriptDownloadProxyConfig *ProxyConfig
// ExcludeRegExp contains a string representation of the regular expression desired for
// deciding which files to exclude from the tar stream
ExcludeRegExp string
// BlockOnBuild prevents s2i from performing a docker build operation
// if one is necessary to execute ONBUILD commands, or to layer source code into
// the container for images that don't have a tar binary available, if the
// image contains ONBUILD commands that would be executed.
BlockOnBuild bool
// HasOnBuild will be set to true if the builder image contains ONBUILD instructions
HasOnBuild bool
// BuildVolumes specifies a list of volumes to mount to container running the
// build.
BuildVolumes []string
// Labels specify labels and their values to be applied to the resulting image. Label keys
// must have non-zero length. The labels defined here override generated labels in case
// they have the same name.
Labels map[string]string
// SourceInfo provides the info about the source to be built rather than relying
// on the Downloader to retrieve it.
SourceInfo *git.SourceInfo
// SecurityOpt are passed as options to the docker containers launched by s2i.
SecurityOpt []string
// KeepSymlinks indicates to copy symlinks as symlinks. Default behavior is to follow
// symlinks and copy files by content.
KeepSymlinks bool
// AsDockerfile indicates the path where the Dockerfile should be written instead of building
// a new image.
AsDockerfile string
// ImageWorkDir is the default working directory for the builder image.
ImageWorkDir string
// ImageScriptsURL is the default location to find the assemble/run scripts for a builder image.
// This url can be a reference within the builder image if the scheme is specified as image://
ImageScriptsURL string
// AddHost Add a line to /etc/hosts for test purpose or private use in LAN. Its format is host:IP,muliple hosts can be added by using multiple --add-host
AddHost []string
// AssembleRuntimeUser specifies the user to run the assemble-runtime script in container
AssembleRuntimeUser string
}
// EnvironmentSpec specifies a single environment variable.
type EnvironmentSpec struct {
Name string
Value string
}
// EnvironmentList contains list of environment variables.
type EnvironmentList []EnvironmentSpec
// ProxyConfig holds proxy configuration.
type ProxyConfig struct {
HTTPProxy *url.URL
HTTPSProxy *url.URL
}
// CGroupLimits holds limits used to constrain container resources.
type CGroupLimits struct {
MemoryLimitBytes int64
CPUShares int64
CPUPeriod int64
CPUQuota int64
MemorySwap int64
Parent string
}
// VolumeSpec represents a single volume mount point.
type VolumeSpec struct {
// Source is a reference to the volume source.
Source string
// Destination is the path to mount the volume to - absolute or relative.
Destination string
// Keep indicates if the mounted data should be kept in the final image.
Keep bool
}
// VolumeList contains list of VolumeSpec.
type VolumeList []VolumeSpec
// DockerConfig contains the configuration for a Docker connection.
type DockerConfig struct {
// Endpoint is the docker network endpoint or socket
Endpoint string
// CertFile is the certificate file path for a TLS connection
CertFile string
// KeyFile is the key file path for a TLS connection
KeyFile string
// CAFile is the certificate authority file path for a TLS connection
CAFile string
// UseTLS indicates if TLS must be used
UseTLS bool
// TLSVerify indicates if TLS peer must be verified
TLSVerify bool
}
// AuthConfig is our abstraction of the Registry authorization information for whatever
// docker client we happen to be based on
type AuthConfig struct {
Username string
Password string
Email string
ServerAddress string
}
// ContainerConfig is the abstraction of the docker client provider (formerly go-dockerclient, now either
// engine-api or kube docker client) container.Config type that is leveraged by s2i or origin
type ContainerConfig struct {
Labels map[string]string
Env []string
}
// Image is the abstraction of the docker client provider (formerly go-dockerclient, now either
// engine-api or kube docker client) Image type that is leveraged by s2i or origin
type Image struct {
ID string
*ContainerConfig
Config *ContainerConfig
}
// Result structure contains information from build process.
type Result struct {
// Success describes whether the build was successful.
Success bool
// Messages is a list of messages from build process.
Messages []string
// WorkingDir describes temporary directory used for downloading sources, scripts and tar operations.
WorkingDir string
// ImageID describes resulting image ID.
ImageID string
// BuildInfo holds information about the result of a build.
BuildInfo BuildInfo
}
// BuildInfo contains information about the build process.
type BuildInfo struct {
// Stages contains details about each build stage.
Stages []StageInfo
// FailureReason is a camel case reason that is used by the machine to reply
// back to the OpenShift builder with information why any of the steps in the
// build failed.
FailureReason FailureReason
}
// StageInfo contains details about a build stage.
type StageInfo struct {
// Name is the identifier for each build stage.
Name StageName
// StartTime identifies when this stage started.
StartTime time.Time
// DurationMilliseconds identifies how long this stage ran.
DurationMilliseconds int64
// Steps contains details about each build step within a build stage.
Steps []StepInfo
}
// StageName is the identifier for each build stage.
type StageName string
// Valid StageNames
const (
// StagePullImages pulls the docker images.
StagePullImages StageName = "PullImages"
//StageAssemble runs the assemble steps.
StageAssemble StageName = "Assemble"
// StageBuild builds the source.
StageBuild StageName = "Build"
// StageCommit commits the container.
StageCommit StageName = "CommitContainer"
// StageRetrieve retrieves artifacts.
StageRetrieve StageName = "RetrieveArtifacts"
)
// StepInfo contains details about a build step.
type StepInfo struct {
// Name is the identifier for each build step.
Name StepName
// StartTime identifies when this step started.
StartTime time.Time
// DurationMilliseconds identifies how long this step ran.
DurationMilliseconds int64
}
// StepName is the identifier for each build step.
type StepName string
// Valid StepNames
const (
// StepPullBuilderImage pulls the builder image.
StepPullBuilderImage StepName = "PullBuilderImage"
// StepPullPreviousImage pulls the previous image for an incremental build.
StepPullPreviousImage StepName = "PullPreviousImage"
// StepPullRuntimeImage pull the runtime image.
StepPullRuntimeImage StepName = "PullRuntimeImage"
// StepAssembleBuildScripts runs the assemble scripts.
StepAssembleBuildScripts StepName = "AssembleBuildScripts"
// StepBuildDockerImage builds the Docker image for layered builds.
StepBuildDockerImage StepName = "BuildDockerImage"
// StepCommitContainer commits the container to the builder image.
StepCommitContainer StepName = "CommitContainer"
// StepRetrievePreviousArtifacts restores archived artifacts from the previous build.
StepRetrievePreviousArtifacts StepName = "RetrievePreviousArtifacts"
)
// StepFailureReason holds the type of failure that occurred during the build
// process.
type StepFailureReason string
// StepFailureMessage holds the detailed message of a failure.
type StepFailureMessage string
// FailureReason holds the type of failure that occurred during the build
// process.
type FailureReason struct {
Reason StepFailureReason
Message StepFailureMessage
}
// InstallResult structure describes the result of install operation
type InstallResult struct {
// Script describes which script this result refers to
Script string
// URL describes from where the script was taken
URL string
// Downloaded describes if download operation happened, this will be true for
// external scripts, but false for scripts from inside the image
Downloaded bool
// Installed describes if script was installed to upload directory
Installed bool
// Error describes last error encountered during install operation
Error error
// FailedSources is a list of sources that were attempted but failed
// when downloading this script
FailedSources []string
}
// DockerNetworkMode specifies the network mode setting for the docker container
type DockerNetworkMode string
const (
// DockerNetworkModeHost places the container in the default (host) network namespace.
DockerNetworkModeHost DockerNetworkMode = "host"
// DockerNetworkModeBridge instructs docker to create a network namespace for this container connected to the docker0 bridge via a veth-pair.
DockerNetworkModeBridge DockerNetworkMode = "bridge"
// DockerNetworkModeContainerPrefix is the string prefix used by NewDockerNetworkModeContainer.
DockerNetworkModeContainerPrefix string = "container:"
// DockerNetworkModeNetworkNamespacePrefix is the string prefix used when sharing a namespace from a CRI-O container.
DockerNetworkModeNetworkNamespacePrefix string = "netns:"
)
// NewDockerNetworkModeContainer creates a DockerNetworkMode value which instructs docker to place the container in the network namespace of an existing container.
// It can be used, for instance, to place the s2i container in the network namespace of the infrastructure container of a k8s pod.
func NewDockerNetworkModeContainer(id string) DockerNetworkMode {
return DockerNetworkMode(DockerNetworkModeContainerPrefix + id)
}
// PullPolicy specifies a type for the method used to retrieve the Docker image
type PullPolicy string
// String implements the String() function of pflags.Value so this can be used as
// command line parameter.
// This method is really used just to show the default value when printing help.
// It will not default the configuration.
func (p *PullPolicy) String() string {
if len(string(*p)) == 0 {
return string(DefaultBuilderPullPolicy)
}
return string(*p)
}
// Type implements the Type() function of pflags.Value interface
func (p *PullPolicy) Type() string {
return "string"
}
// Set implements the Set() function of pflags.Value interface
// The valid options are "always", "never" or "if-not-present"
func (p *PullPolicy) Set(v string) error {
switch v {
case "always":
*p = PullAlways
case "never":
*p = PullNever
case "if-not-present":
*p = PullIfNotPresent
default:
return fmt.Errorf("invalid value %q, valid values are: always, never or if-not-present", v)
}
return nil
}
// IsInvalidFilename verifies if the provided filename contains malicious
// characters.
func IsInvalidFilename(name string) bool {
return strings.ContainsAny(name, invalidFilenameCharacters)
}
// Set implements the Set() function of pflags.Value interface.
// This function parses the string that contains source:destination pair.
// When the destination is not specified, the source get copied into current
// working directory in container.
func (l *VolumeList) Set(value string) error {
volumes := strings.Split(value, ";")
newVols := make([]VolumeSpec, len(volumes))
for i, v := range volumes {
spec, err := l.parseSpec(v)
if err != nil {
return err
}
newVols[i] = *spec
}
*l = append(*l, newVols...)
return nil
}
func (l *VolumeList) parseSpec(value string) (*VolumeSpec, error) {
if len(value) == 0 {
return nil, errors.New("invalid format, must be source:destination")
}
var mount []string
pos := strings.LastIndex(value, ":")
if pos == -1 {
mount = []string{value, ""}
} else {
mount = []string{value[:pos], value[pos+1:]}
}
mount[0] = strings.Trim(mount[0], `"'`)
mount[1] = strings.Trim(mount[1], `"'`)
s := &VolumeSpec{Source: filepath.Clean(mount[0]), Destination: filepath.ToSlash(filepath.Clean(mount[1]))}
if IsInvalidFilename(s.Source) || IsInvalidFilename(s.Destination) {
return nil, fmt.Errorf("invalid characters in filename: %q", value)
}
return s, nil
}
// String implements the String() function of pflags.Value interface.
func (l *VolumeList) String() string {
result := []string{}
for _, i := range *l {
result = append(result, strings.Join([]string{i.Source, i.Destination}, ":"))
}
return strings.Join(result, ",")
}
// Type implements the Type() function of pflags.Value interface.
func (l *VolumeList) Type() string {
return "string"
}
// Set implements the Set() function of pflags.Value interface.
func (e *EnvironmentList) Set(value string) error {
parts := strings.SplitN(value, "=", 2)
if len(parts) != 2 || len(parts[0]) == 0 {
return fmt.Errorf("invalid environment format %q, must be NAME=VALUE", value)
}
if strings.Contains(parts[1], ",") && strings.Contains(parts[1], "=") {
glog.Warningf("DEPRECATED: Use multiple -e flags to specify multiple environment variables instead of comma (%q)", value)
}
*e = append(*e, EnvironmentSpec{
Name: strings.TrimSpace(parts[0]),
Value: strings.TrimSpace(parts[1]),
})
return nil
}
// String implements the String() function of pflags.Value interface.
func (e *EnvironmentList) String() string {
result := []string{}
for _, i := range *e {
result = append(result, strings.Join([]string{i.Name, i.Value}, "="))
}
return strings.Join(result, ",")
}
// Type implements the Type() function of pflags.Value interface.
func (e *EnvironmentList) Type() string {
return "string"
}
// AsBinds converts the list of volume definitions to go-dockerclient compatible
// list of bind mounts.
func (l *VolumeList) AsBinds() []string {
result := make([]string, len(*l))
for index, v := range *l {
result[index] = strings.Join([]string{v.Source, v.Destination}, ":")
}
return result
}
package validation
import (
"fmt"
"strings"
"github.com/docker/distribution/reference"
"github.com/openshift/source-to-image/pkg/api"
)
// ValidateConfig returns a list of error from validation.
func ValidateConfig(config *api.Config) []Error {
allErrs := []Error{}
if len(config.BuilderImage) == 0 {
allErrs = append(allErrs, NewFieldRequired("builderImage"))
}
switch config.BuilderPullPolicy {
case api.PullNever, api.PullAlways, api.PullIfNotPresent:
default:
allErrs = append(allErrs, NewFieldInvalidValue("builderPullPolicy"))
}
if config.DockerConfig == nil || len(config.DockerConfig.Endpoint) == 0 {
allErrs = append(allErrs, NewFieldRequired("dockerConfig.endpoint"))
}
if config.DockerNetworkMode != "" && !validateDockerNetworkMode(config.DockerNetworkMode) {
allErrs = append(allErrs, NewFieldInvalidValue("dockerNetworkMode"))
}
if config.Labels != nil {
for k := range config.Labels {
if len(k) == 0 {
allErrs = append(allErrs, NewFieldInvalidValue("labels"))
}
}
}
if config.Tag != "" {
if err := validateDockerReference(config.Tag); err != nil {
allErrs = append(allErrs, NewFieldInvalidValueWithReason("tag", err.Error()))
}
}
return allErrs
}
// validateDockerNetworkMode checks wether the network mode conforms to the docker remote API specification (v1.19)
// Supported values are: bridge, host, container:<name|id>, and netns:/proc/<pid>/ns/net
func validateDockerNetworkMode(mode api.DockerNetworkMode) bool {
switch mode {
case api.DockerNetworkModeBridge, api.DockerNetworkModeHost:
return true
}
if strings.HasPrefix(string(mode), api.DockerNetworkModeContainerPrefix) {
return true
}
if strings.HasPrefix(string(mode), api.DockerNetworkModeNetworkNamespacePrefix) {
return true
}
return false
}
func validateDockerReference(ref string) error {
_, err := reference.Parse(ref)
return err
}
// NewFieldRequired returns a *ValidationError indicating "value required"
func NewFieldRequired(field string) Error {
return Error{Type: ErrorTypeRequired, Field: field}
}
// NewFieldInvalidValue returns a ValidationError indicating "invalid value"
func NewFieldInvalidValue(field string) Error {
return Error{Type: ErrorInvalidValue, Field: field}
}
// NewFieldInvalidValueWithReason returns a ValidationError indicating "invalid value" and a reason for the error
func NewFieldInvalidValueWithReason(field, reason string) Error {
return Error{Type: ErrorInvalidValue, Field: field, Reason: reason}
}
// ErrorType is a machine readable value providing more detail about why a field
// is invalid.
type ErrorType string
const (
// ErrorTypeRequired is used to report required values that are not provided
// (e.g. empty strings, null values, or empty arrays).
ErrorTypeRequired ErrorType = "FieldValueRequired"
// ErrorInvalidValue is used to report values that do not conform to the
// expected schema.
ErrorInvalidValue ErrorType = "InvalidValue"
)
// Error is an implementation of the 'error' interface, which represents an
// error of validation.
type Error struct {
Type ErrorType
Field string
Reason string
}
func (v Error) Error() string {
var msg string
switch v.Type {
case ErrorInvalidValue:
msg = fmt.Sprintf("Invalid value specified for %q", v.Field)
case ErrorTypeRequired:
msg = fmt.Sprintf("Required value not specified for %q", v.Field)
default:
msg = fmt.Sprintf("%s: %s", v.Type, v.Field)
}
if len(v.Reason) > 0 {
msg = fmt.Sprintf("%s: %s", msg, v.Reason)
}
return msg
}
package dockerfile
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/constants"
"github.com/openshift/source-to-image/pkg/build"
s2ierr "github.com/openshift/source-to-image/pkg/errors"
"github.com/openshift/source-to-image/pkg/ignore"
"github.com/openshift/source-to-image/pkg/scm"
"github.com/openshift/source-to-image/pkg/scm/downloaders/file"
"github.com/openshift/source-to-image/pkg/scm/git"
"github.com/openshift/source-to-image/pkg/scripts"
"github.com/openshift/source-to-image/pkg/util"
"github.com/openshift/source-to-image/pkg/util/fs"
utilglog "github.com/openshift/source-to-image/pkg/util/glog"
utilstatus "github.com/openshift/source-to-image/pkg/util/status"
"github.com/openshift/source-to-image/pkg/util/user"
)
const (
defaultDestination = "/tmp"
defaultScriptsDir = "/usr/libexec/s2i"
)
var (
glog = utilglog.StderrLog
// List of directories that needs to be present inside working dir
workingDirs = []string{
constants.UploadScripts,
constants.Source,
constants.DefaultScripts,
constants.UserScripts,
}
)
// Dockerfile builders produce a Dockerfile rather than an image.
// Building the dockerfile w/ the right context will result in
// an application image being produced.
type Dockerfile struct {
fs fs.FileSystem
uploadScriptsDir string
uploadSrcDir string
sourceInfo *git.SourceInfo
result *api.Result
ignorer build.Ignorer
}
// New creates a Dockerfile builder.
func New(config *api.Config, fs fs.FileSystem) (*Dockerfile, error) {
return &Dockerfile{
fs: fs,
// where we will get the assemble/run scripts from on the host machine,
// if any are provided.
uploadScriptsDir: constants.UploadScripts,
uploadSrcDir: constants.Source,
result: &api.Result{},
ignorer: &ignore.DockerIgnorer{},
}, nil
}
// Build produces a Dockerfile that when run with the correct filesystem
// context, will produce the application image.
func (builder *Dockerfile) Build(config *api.Config) (*api.Result, error) {
// Handle defaulting of the configuration that is unique to the dockerfile strategy
if strings.HasSuffix(config.AsDockerfile, string(os.PathSeparator)) {
config.AsDockerfile = config.AsDockerfile + "Dockerfile"
}
if len(config.AssembleUser) == 0 {
config.AssembleUser = "1001"
}
if !user.IsUserAllowed(config.AssembleUser, &config.AllowedUIDs) {
builder.setFailureReason(utilstatus.ReasonAssembleUserForbidden, utilstatus.ReasonMessageAssembleUserForbidden)
return builder.result, s2ierr.NewUserNotAllowedError(config.AssembleUser, false)
}
dir, _ := filepath.Split(config.AsDockerfile)
if len(dir) == 0 {
dir = "."
}
config.PreserveWorkingDir = true
config.WorkingDir = dir
if config.BuilderImage == "" {
builder.setFailureReason(utilstatus.ReasonGenericS2IBuildFailed, utilstatus.ReasonMessageGenericS2iBuildFailed)
return builder.result, errors.New("builder image name cannot be empty")
}
if err := builder.Prepare(config); err != nil {
return builder.result, err
}
if err := builder.CreateDockerfile(config); err != nil {
builder.setFailureReason(utilstatus.ReasonDockerfileCreateFailed, utilstatus.ReasonMessageDockerfileCreateFailed)
return builder.result, err
}
builder.result.Success = true
return builder.result, nil
}
// CreateDockerfile takes the various inputs and creates the Dockerfile used by
// the docker cmd to create the image produced by s2i.
func (builder *Dockerfile) CreateDockerfile(config *api.Config) error {
glog.V(4).Infof("Constructing image build context directory at %s", config.WorkingDir)
buffer := bytes.Buffer{}
if len(config.ImageWorkDir) == 0 {
config.ImageWorkDir = "/opt/app-root/src"
}
imageUser := config.AssembleUser
// where files will land inside the new image.
scriptsDestDir := filepath.Join(getDestination(config), "scripts")
sourceDestDir := filepath.Join(getDestination(config), "src")
artifactsDestDir := filepath.Join(getDestination(config), "artifacts")
artifactsTar := sanitize(filepath.ToSlash(filepath.Join(defaultDestination, "artifacts.tar")))
// hasAllScripts indicates that we blindly trust all scripts are provided in the image scripts dir
imageScriptsDir, hasAllScripts := getImageScriptsDir(config)
var providedScripts map[string]bool
if !hasAllScripts {
providedScripts = scanScripts(filepath.Join(config.WorkingDir, builder.uploadScriptsDir))
}
if config.Incremental {
imageTag := util.FirstNonEmpty(config.IncrementalFromTag, config.Tag)
if len(imageTag) == 0 {
return errors.New("Image tag is missing for incremental build")
}
// Incremental builds run via a multistage Dockerfile
buffer.WriteString(fmt.Sprintf("FROM %s as cached\n", imageTag))
var artifactsScript string
if _, provided := providedScripts[constants.SaveArtifacts]; provided {
// switch to root to COPY and chown content
glog.V(2).Infof("Override save-artifacts script is included in directory %q", builder.uploadScriptsDir)
buffer.WriteString("# Copying in override save-artifacts script\n")
buffer.WriteString("USER root\n")
artifactsScript = sanitize(filepath.ToSlash(filepath.Join(scriptsDestDir, "save-artifacts")))
uploadScript := sanitize(filepath.ToSlash(filepath.Join(builder.uploadScriptsDir, "save-artifacts")))
buffer.WriteString(fmt.Sprintf("COPY %s %s\n", uploadScript, artifactsScript))
buffer.WriteString(fmt.Sprintf("RUN chown %s:0 %s\n", sanitize(imageUser), artifactsScript))
} else {
buffer.WriteString(fmt.Sprintf("# Save-artifacts script sourced from builder image based on user input or image metadata.\n"))
artifactsScript = sanitize(filepath.ToSlash(filepath.Join(imageScriptsDir, "save-artifacts")))
}
// switch to the image user if it is not root
if len(imageUser) > 0 && imageUser != "root" {
buffer.WriteString(fmt.Sprintf("USER %s\n", imageUser))
}
buffer.WriteString(fmt.Sprintf("RUN if [ -s %[1]s ]; then %[1]s > %[2]s; else touch %[2]s; fi\n", artifactsScript, artifactsTar))
}
// main stage of the Dockerfile
buffer.WriteString(fmt.Sprintf("FROM %s\n", config.BuilderImage))
imageLabels := util.GenerateOutputImageLabels(builder.sourceInfo, config)
for k, v := range config.Labels {
imageLabels[k] = v
}
if len(imageLabels) > 0 {
first := true
buffer.WriteString("LABEL ")
for k, v := range imageLabels {
if !first {
buffer.WriteString(fmt.Sprintf(" \\\n "))
}
buffer.WriteString(fmt.Sprintf("%q=%q", k, v))
first = false
}
buffer.WriteString("\n")
}
env := createBuildEnvironment(config.WorkingDir, config.Environment)
buffer.WriteString(fmt.Sprintf("%s", env))
// run as root to COPY and chown source content
buffer.WriteString("USER root\n")
chownList := make([]string, 0)
if config.Incremental {
// COPY artifacts.tar from the `cached` stage
buffer.WriteString(fmt.Sprintf("COPY --from=cached %[1]s %[1]s\n", artifactsTar))
chownList = append(chownList, artifactsTar)
}
if len(providedScripts) > 0 {
// Only COPY scripts dir if required scripts are present and needed.
// Even if the "scripts" dir exists, the COPY would fail if it was empty.
glog.V(2).Infof("Override scripts are included in directory %q", builder.uploadScriptsDir)
scriptsDest := sanitize(filepath.ToSlash(scriptsDestDir))
buffer.WriteString("# Copying in override assemble/run scripts\n")
buffer.WriteString(fmt.Sprintf("COPY %s %s\n", sanitize(filepath.ToSlash(builder.uploadScriptsDir)), scriptsDest))
chownList = append(chownList, scriptsDest)
}
// copy in the user's source code.
buffer.WriteString("# Copying in source code\n")
sourceDest := sanitize(filepath.ToSlash(sourceDestDir))
buffer.WriteString(fmt.Sprintf("COPY %s %s\n", sanitize(filepath.ToSlash(builder.uploadSrcDir)), sourceDest))
chownList = append(chownList, sourceDest)
// add injections
glog.V(4).Infof("Processing injected inputs: %#v", config.Injections)
config.Injections = util.FixInjectionsWithRelativePath(config.ImageWorkDir, config.Injections)
glog.V(4).Infof("Processed injected inputs: %#v", config.Injections)
if len(config.Injections) > 0 {
buffer.WriteString("# Copying in injected content\n")
}
for _, injection := range config.Injections {
src := sanitize(filepath.ToSlash(filepath.Join(constants.Injections, injection.Source)))
dest := sanitize(filepath.ToSlash(injection.Destination))
buffer.WriteString(fmt.Sprintf("COPY %s %s\n", src, dest))
chownList = append(chownList, dest)
}
// chown directories COPYed to image
if len(chownList) > 0 {
buffer.WriteString("# Change file ownership to the assemble user. Builder image must support chown command.\n")
buffer.WriteString(fmt.Sprintf("RUN chown -R %s:0", sanitize(imageUser)))
for _, dir := range chownList {
buffer.WriteString(fmt.Sprintf(" %s", dir))
}
buffer.WriteString("\n")
}
// run remaining commands as the image user
if len(imageUser) > 0 && imageUser != "root" {
buffer.WriteString(fmt.Sprintf("USER %s\n", imageUser))
}
if config.Incremental {
buffer.WriteString("# Extract artifact content\n")
buffer.WriteString(fmt.Sprintf("RUN if [ -s %[1]s ]; then mkdir -p %[2]s; tar -xf %[1]s -C %[2]s; fi && \\\n", artifactsTar, sanitize(filepath.ToSlash(artifactsDestDir))))
buffer.WriteString(fmt.Sprintf(" rm %s\n", artifactsTar))
}
if _, provided := providedScripts[constants.Assemble]; provided {
buffer.WriteString(fmt.Sprintf("RUN %s\n", sanitize(filepath.ToSlash(filepath.Join(scriptsDestDir, "assemble")))))
} else {
buffer.WriteString(fmt.Sprintf("# Assemble script sourced from builder image based on user input or image metadata.\n"))
buffer.WriteString(fmt.Sprintf("# If this file does not exist in the image, the build will fail.\n"))
buffer.WriteString(fmt.Sprintf("RUN %s\n", sanitize(filepath.ToSlash(filepath.Join(imageScriptsDir, "assemble")))))
}
filesToDelete, err := util.ListFilesToTruncate(builder.fs, config.Injections)
if err != nil {
return err
}
if len(filesToDelete) > 0 {
wroteRun := false
buffer.WriteString("# Cleaning up injected secret content\n")
for _, file := range filesToDelete {
if !wroteRun {
buffer.WriteString(fmt.Sprintf("RUN rm %s", file))
wroteRun = true
continue
}
buffer.WriteString(fmt.Sprintf(" && \\\n"))
buffer.WriteString(fmt.Sprintf(" rm %s", file))
}
buffer.WriteString("\n")
}
if _, provided := providedScripts[constants.Run]; provided {
buffer.WriteString(fmt.Sprintf("CMD %s\n", sanitize(filepath.ToSlash(filepath.Join(scriptsDestDir, "run")))))
} else {
buffer.WriteString(fmt.Sprintf("# Run script sourced from builder image based on user input or image metadata.\n"))
buffer.WriteString(fmt.Sprintf("# If this file does not exist in the image, the build will fail.\n"))
buffer.WriteString(fmt.Sprintf("CMD %s\n", sanitize(filepath.ToSlash(filepath.Join(imageScriptsDir, "run")))))
}
if err := builder.fs.WriteFile(filepath.Join(config.AsDockerfile), buffer.Bytes()); err != nil {
return err
}
glog.V(2).Infof("Wrote custom Dockerfile to %s", config.AsDockerfile)
return nil
}
// Prepare prepares the source code and tar for build.
// NOTE: this func serves both the sti and onbuild strategies, as the OnBuild
// struct Build func leverages the STI struct Prepare func directly below.
func (builder *Dockerfile) Prepare(config *api.Config) error {
var err error
if len(config.WorkingDir) == 0 {
if config.WorkingDir, err = builder.fs.CreateWorkingDirectory(); err != nil {
builder.setFailureReason(utilstatus.ReasonFSOperationFailed, utilstatus.ReasonMessageFSOperationFailed)
return err
}
}
builder.result.WorkingDir = config.WorkingDir
// Setup working directories
for _, v := range workingDirs {
if err = builder.fs.MkdirAllWithPermissions(filepath.Join(config.WorkingDir, v), 0755); err != nil {
builder.setFailureReason(utilstatus.ReasonFSOperationFailed, utilstatus.ReasonMessageFSOperationFailed)
return err
}
}
// Default - install scripts specified by image metadata.
// Typically this will point to an image:// URL, and no scripts are downloaded.
// However, this is not guaranteed.
builder.installScripts(config.ImageScriptsURL, config)
// Fetch sources, since their .s2i/bin might contain s2i scripts which override defaults.
if config.Source != nil {
downloader, err := scm.DownloaderForSource(builder.fs, config.Source, config.ForceCopy)
if err != nil {
builder.setFailureReason(utilstatus.ReasonFetchSourceFailed, utilstatus.ReasonMessageFetchSourceFailed)
return err
}
if builder.sourceInfo, err = downloader.Download(config); err != nil {
builder.setFailureReason(utilstatus.ReasonFetchSourceFailed, utilstatus.ReasonMessageFetchSourceFailed)
switch err.(type) {
case file.RecursiveCopyError:
return fmt.Errorf("input source directory contains the target directory for the build, check that your Dockerfile output path does not reside within your input source path: %v", err)
}
return err
}
if config.SourceInfo != nil {
builder.sourceInfo = config.SourceInfo
}
}
// Install scripts provided by user, overriding all others.
// This _could_ be an image:// URL, which would override any scripts above.
urlScripts := builder.installScripts(config.ScriptsURL, config)
// If a ScriptsURL was specified, but no scripts were downloaded from it, throw an error
if len(config.ScriptsURL) > 0 {
failedCount := 0
for _, result := range urlScripts {
if util.Includes(result.FailedSources, scripts.ScriptURLHandler) {
failedCount++
}
}
if failedCount == len(urlScripts) {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonScriptsFetchFailed,
utilstatus.ReasonMessageScriptsFetchFailed,
)
return fmt.Errorf("could not download any scripts from URL %v", config.ScriptsURL)
}
}
// Stage any injection(secrets) content into the working dir so the dockerfile can reference it.
for i, injection := range config.Injections {
// strip the C: from windows paths because it's not valid in the middle of a path
// like upload/injections/C:/tempdir/injection1
trimmedSrc := strings.TrimPrefix(injection.Source, filepath.VolumeName(injection.Source))
dst := filepath.Join(config.WorkingDir, constants.Injections, trimmedSrc)
glog.V(4).Infof("Copying injection content from %s to %s", injection.Source, dst)
if err := builder.fs.CopyContents(injection.Source, dst, nil); err != nil {
builder.setFailureReason(utilstatus.ReasonGenericS2IBuildFailed, utilstatus.ReasonMessageGenericS2iBuildFailed)
return err
}
config.Injections[i].Source = trimmedSrc
}
// see if there is a .s2iignore file, and if so, read in the patterns and then
// search and delete on them.
err = builder.ignorer.Ignore(config)
if err != nil {
builder.setFailureReason(utilstatus.ReasonGenericS2IBuildFailed, utilstatus.ReasonMessageGenericS2iBuildFailed)
return err
}
return nil
}
// installScripts installs scripts at the provided URL to the Dockerfile context
func (builder *Dockerfile) installScripts(scriptsURL string, config *api.Config) []api.InstallResult {
scriptInstaller := scripts.NewInstaller(
"",
scriptsURL,
config.ScriptDownloadProxyConfig,
nil,
api.AuthConfig{},
builder.fs,
)
// all scripts are optional, we trust the image contains scripts if we don't find them
// in the source repo.
return scriptInstaller.InstallOptional(append(scripts.RequiredScripts, scripts.OptionalScripts...), config.WorkingDir)
}
// setFailureReason sets the builder's failure reason with the given reason and message.
func (builder *Dockerfile) setFailureReason(reason api.StepFailureReason, message api.StepFailureMessage) {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(reason, message)
}
// getDestination returns the destination directory from the config.
func getDestination(config *api.Config) string {
destination := config.Destination
if len(destination) == 0 {
destination = defaultDestination
}
return destination
}
// getImageScriptsDir returns the directory containing the builder image scripts and a bool
// indicating that the directory is expected to contain all s2i scripts
func getImageScriptsDir(config *api.Config) (string, bool) {
if strings.HasPrefix(config.ScriptsURL, "image://") {
return strings.TrimPrefix(config.ScriptsURL, "image://"), true
}
if strings.HasPrefix(config.ImageScriptsURL, "image://") {
return strings.TrimPrefix(config.ImageScriptsURL, "image://"), false
}
return defaultScriptsDir, false
}
// scanScripts returns a map of provided s2i scripts
func scanScripts(name string) map[string]bool {
scriptsMap := make(map[string]bool)
items, err := ioutil.ReadDir(name)
if os.IsNotExist(err) {
glog.Warningf("Unable to access directory %q: %v", name, err)
}
if err != nil || len(items) == 0 {
return scriptsMap
}
assembleProvided := false
runProvided := false
saveArtifactsProvided := false
for _, f := range items {
glog.V(2).Infof("found override script file %s", f.Name())
if f.Name() == constants.Run {
runProvided = true
scriptsMap[constants.Run] = true
} else if f.Name() == constants.Assemble {
assembleProvided = true
scriptsMap[constants.Assemble] = true
} else if f.Name() == constants.SaveArtifacts {
saveArtifactsProvided = true
scriptsMap[constants.SaveArtifacts] = true
}
if runProvided && assembleProvided && saveArtifactsProvided {
break
}
}
return scriptsMap
}
func includes(arr []string, str string) bool {
for _, s := range arr {
if s == str {
return true
}
}
return false
}
func sanitize(s string) string {
return strings.Replace(s, "\n", "\\n", -1)
}
func createBuildEnvironment(sourcePath string, cfgEnv api.EnvironmentList) string {
s2iEnv, err := scripts.GetEnvironment(filepath.Join(sourcePath, constants.Source))
if err != nil {
glog.V(3).Infof("No user environment provided (%v)", err)
}
return scripts.ConvertEnvironmentToDocker(append(s2iEnv, cfgEnv...))
}
package layered
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"time"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/constants"
"github.com/openshift/source-to-image/pkg/build"
"github.com/openshift/source-to-image/pkg/docker"
s2ierr "github.com/openshift/source-to-image/pkg/errors"
"github.com/openshift/source-to-image/pkg/tar"
"github.com/openshift/source-to-image/pkg/util/fs"
utilglog "github.com/openshift/source-to-image/pkg/util/glog"
utilstatus "github.com/openshift/source-to-image/pkg/util/status"
)
var glog = utilglog.StderrLog
const defaultDestination = "/tmp"
// A Layered builder builds images by first performing a docker build to inject
// (layer) the source code and s2i scripts into the builder image, prior to
// running the new image with the assemble script. This is necessary when the
// builder image does not include "sh" and "tar" as those tools are needed
// during the normal source injection process.
type Layered struct {
config *api.Config
docker docker.Docker
fs fs.FileSystem
tar tar.Tar
scripts build.ScriptsHandler
hasOnBuild bool
}
// New creates a Layered builder.
func New(client docker.Client, config *api.Config, fs fs.FileSystem, scripts build.ScriptsHandler, overrides build.Overrides) (*Layered, error) {
excludePattern, err := regexp.Compile(config.ExcludeRegExp)
if err != nil {
return nil, err
}
d := docker.New(client, config.PullAuthentication)
tarHandler := tar.New(fs)
tarHandler.SetExclusionPattern(excludePattern)
return &Layered{
docker: d,
config: config,
fs: fs,
tar: tarHandler,
scripts: scripts,
}, nil
}
// getDestination returns the destination directory from the config.
func getDestination(config *api.Config) string {
destination := config.Destination
if len(destination) == 0 {
destination = defaultDestination
}
return destination
}
// checkValidDirWithContents returns true if the parameter provided is a valid,
// accessible and non-empty directory.
func checkValidDirWithContents(name string) bool {
items, err := ioutil.ReadDir(name)
if os.IsNotExist(err) {
glog.Warningf("Unable to access directory %q: %v", name, err)
}
return !(err != nil || len(items) == 0)
}
// CreateDockerfile takes the various inputs and creates the Dockerfile used by
// the docker cmd to create the image produced by s2i.
func (builder *Layered) CreateDockerfile(config *api.Config) error {
buffer := bytes.Buffer{}
user, err := builder.docker.GetImageUser(builder.config.BuilderImage)
if err != nil {
return err
}
scriptsDir := filepath.Join(getDestination(config), "scripts")
sourcesDir := filepath.Join(getDestination(config), "src")
uploadScriptsDir := path.Join(config.WorkingDir, constants.UploadScripts)
buffer.WriteString(fmt.Sprintf("FROM %s\n", builder.config.BuilderImage))
// only COPY scripts dir if required scripts are present, i.e. the dir is not empty;
// even if the "scripts" dir exists, the COPY would fail if it was empty
scriptsIncluded := checkValidDirWithContents(uploadScriptsDir)
if scriptsIncluded {
glog.V(2).Infof("The scripts are included in %q directory", uploadScriptsDir)
buffer.WriteString(fmt.Sprintf("COPY scripts %s\n", filepath.ToSlash(scriptsDir)))
} else {
// if an err on reading or opening dir, can't copy it
glog.V(2).Infof("Could not gather scripts from the directory %q", uploadScriptsDir)
}
buffer.WriteString(fmt.Sprintf("COPY src %s\n", filepath.ToSlash(sourcesDir)))
//TODO: We need to account for images that may not have chown. There is a proposal
// to specify the owner for COPY here: https://github.com/docker/docker/pull/28499
if len(user) > 0 {
buffer.WriteString("USER root\n")
if scriptsIncluded {
buffer.WriteString(fmt.Sprintf("RUN chown -R %s -- %s %s\n", user, filepath.ToSlash(scriptsDir), filepath.ToSlash(sourcesDir)))
} else {
buffer.WriteString(fmt.Sprintf("RUN chown -R %s -- %s\n", user, filepath.ToSlash(sourcesDir)))
}
buffer.WriteString(fmt.Sprintf("USER %s\n", user))
}
uploadDir := filepath.Join(builder.config.WorkingDir, "upload")
if err := builder.fs.WriteFile(filepath.Join(uploadDir, "Dockerfile"), buffer.Bytes()); err != nil {
return err
}
glog.V(2).Infof("Writing custom Dockerfile to %s", uploadDir)
return nil
}
// Build handles the `docker build` equivalent execution, returning the
// success/failure details.
func (builder *Layered) Build(config *api.Config) (*api.Result, error) {
buildResult := &api.Result{}
if config.HasOnBuild && config.BlockOnBuild {
buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonOnBuildForbidden,
utilstatus.ReasonMessageOnBuildForbidden,
)
return buildResult, errors.New("builder image uses ONBUILD instructions but ONBUILD is not allowed")
}
if config.BuilderImage == "" {
buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return buildResult, errors.New("builder image name cannot be empty")
}
if err := builder.CreateDockerfile(config); err != nil {
buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonDockerfileCreateFailed,
utilstatus.ReasonMessageDockerfileCreateFailed,
)
return buildResult, err
}
glog.V(2).Info("Creating application source code image")
tarStream := builder.tar.CreateTarStreamReader(filepath.Join(config.WorkingDir, "upload"), false)
defer tarStream.Close()
newBuilderImage := fmt.Sprintf("s2i-layered-temp-image-%d", time.Now().UnixNano())
outReader, outWriter := io.Pipe()
opts := docker.BuildImageOptions{
Name: newBuilderImage,
Stdin: tarStream,
Stdout: outWriter,
CGroupLimits: config.CGroupLimits,
}
docker.StreamContainerIO(outReader, nil, func(s string) { glog.V(2).Info(s) })
glog.V(2).Infof("Building new image %s with scripts and sources already inside", newBuilderImage)
startTime := time.Now()
err := builder.docker.BuildImage(opts)
buildResult.BuildInfo.Stages = api.RecordStageAndStepInfo(buildResult.BuildInfo.Stages, api.StageBuild, api.StepBuildDockerImage, startTime, time.Now())
if err != nil {
buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonDockerImageBuildFailed,
utilstatus.ReasonMessageDockerImageBuildFailed,
)
return buildResult, err
}
// upon successful build we need to modify current config
builder.config.LayeredBuild = true
// new image name
builder.config.BuilderImage = newBuilderImage
// see CreateDockerfile, conditional copy, location of scripts
scriptsIncluded := checkValidDirWithContents(path.Join(config.WorkingDir, constants.UploadScripts))
glog.V(2).Infof("Scripts dir has contents %v", scriptsIncluded)
if scriptsIncluded {
builder.config.ScriptsURL = "image://" + path.Join(getDestination(config), "scripts")
} else {
var err error
builder.config.ScriptsURL, err = builder.docker.GetScriptsURL(newBuilderImage)
if err != nil {
buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return buildResult, err
}
}
glog.V(2).Infof("Building %s using sti-enabled image", builder.config.Tag)
startTime = time.Now()
err = builder.scripts.Execute(constants.Assemble, config.AssembleUser, builder.config)
buildResult.BuildInfo.Stages = api.RecordStageAndStepInfo(buildResult.BuildInfo.Stages, api.StageAssemble, api.StepAssembleBuildScripts, startTime, time.Now())
if err != nil {
buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonAssembleFailed,
utilstatus.ReasonMessageAssembleFailed,
)
switch e := err.(type) {
case s2ierr.ContainerError:
return buildResult, s2ierr.NewAssembleError(builder.config.Tag, e.Output, e)
default:
return buildResult, err
}
}
buildResult.Success = true
return buildResult, nil
}
package onbuild
import (
"errors"
"path/filepath"
"regexp"
"github.com/openshift/source-to-image/pkg/util/fs"
utilglog "github.com/openshift/source-to-image/pkg/util/glog"
)
var glog = utilglog.StderrLog
var validEntrypoints = []*regexp.Regexp{
regexp.MustCompile(`^run(\.sh)?$`),
regexp.MustCompile(`^start(\.sh)?$`),
regexp.MustCompile(`^exec(\.sh)?$`),
regexp.MustCompile(`^execute(\.sh)?$`),
}
// GuessEntrypoint tries to guess the valid entrypoint from the source code
// repository. The valid entrypoints are defined above (run,start,exec,execute)
func GuessEntrypoint(fs fs.FileSystem, sourceDir string) (string, error) {
files, err := fs.ReadDir(sourceDir)
if err != nil {
return "", err
}
for _, f := range files {
if f.IsDir() || !f.Mode().IsRegular() {
continue
}
if isValidEntrypoint(fs, filepath.Join(sourceDir, f.Name())) {
glog.V(2).Infof("Found valid ENTRYPOINT: %s", f.Name())
return f.Name(), nil
}
}
return "", errors.New("no valid entrypoint specified")
}
// isValidEntrypoint checks if the given file exists and if it is a regular
// file. Valid ENTRYPOINT must be an executable file, so the executable bit must
// be set.
func isValidEntrypoint(fs fs.FileSystem, path string) bool {
stat, err := fs.Stat(path)
if err != nil {
return false
}
found := false
for _, pattern := range validEntrypoints {
if pattern.MatchString(stat.Name()) {
found = true
break
}
}
if !found {
return false
}
mode := stat.Mode()
return mode&0111 != 0
}
package onbuild
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/constants"
"github.com/openshift/source-to-image/pkg/build"
"github.com/openshift/source-to-image/pkg/build/strategies/sti"
"github.com/openshift/source-to-image/pkg/docker"
"github.com/openshift/source-to-image/pkg/ignore"
"github.com/openshift/source-to-image/pkg/scm"
"github.com/openshift/source-to-image/pkg/scm/git"
"github.com/openshift/source-to-image/pkg/scripts"
"github.com/openshift/source-to-image/pkg/tar"
"github.com/openshift/source-to-image/pkg/util/cmd"
"github.com/openshift/source-to-image/pkg/util/fs"
utilstatus "github.com/openshift/source-to-image/pkg/util/status"
)
// OnBuild strategy executes the simple Docker build in case the image does not
// support STI scripts but has ONBUILD instructions recorded.
type OnBuild struct {
docker docker.Docker
git git.Git
fs fs.FileSystem
tar tar.Tar
source build.SourceHandler
garbage build.Cleaner
}
type onBuildSourceHandler struct {
build.Downloader
build.Preparer
build.Ignorer
}
// New returns a new instance of OnBuild builder
func New(client docker.Client, config *api.Config, fs fs.FileSystem, overrides build.Overrides) (*OnBuild, error) {
dockerHandler := docker.New(client, config.PullAuthentication)
builder := &OnBuild{
docker: dockerHandler,
git: git.New(fs, cmd.NewCommandRunner()),
fs: fs,
tar: tar.New(fs),
}
// Use STI Prepare() and download the 'run' script optionally.
s, err := sti.New(client, config, fs, overrides)
if err != nil {
return nil, err
}
s.SetScripts([]string{}, []string{constants.Assemble, constants.Run})
downloader := overrides.Downloader
if downloader == nil {
downloader, err = scm.DownloaderForSource(builder.fs, config.Source, config.ForceCopy)
if err != nil {
return nil, err
}
}
builder.source = onBuildSourceHandler{
Downloader: downloader,
Preparer: s,
Ignorer: &ignore.DockerIgnorer{},
}
builder.garbage = build.NewDefaultCleaner(builder.fs, builder.docker)
return builder, nil
}
// Build executes the ONBUILD kind of build
func (builder *OnBuild) Build(config *api.Config) (*api.Result, error) {
buildResult := &api.Result{}
if config.BlockOnBuild {
buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonOnBuildForbidden,
utilstatus.ReasonMessageOnBuildForbidden,
)
return buildResult, fmt.Errorf("builder image uses ONBUILD instructions but ONBUILD is not allowed")
}
glog.V(2).Info("Preparing the source code for build")
// Change the installation directory for this config to store scripts inside
// the application root directory.
if err := builder.source.Prepare(config); err != nil {
return buildResult, err
}
// If necessary, copy the STI scripts into application root directory
builder.copySTIScripts(config)
glog.V(2).Info("Creating application Dockerfile")
if err := builder.CreateDockerfile(config); err != nil {
buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonDockerfileCreateFailed,
utilstatus.ReasonMessageDockerfileCreateFailed,
)
return buildResult, err
}
glog.V(2).Info("Creating application source code image")
tarStream := builder.tar.CreateTarStreamReader(filepath.Join(config.WorkingDir, "upload", "src"), false)
defer tarStream.Close()
outReader, outWriter := io.Pipe()
go io.Copy(os.Stdout, outReader)
opts := docker.BuildImageOptions{
Name: config.Tag,
Stdin: tarStream,
Stdout: outWriter,
CGroupLimits: config.CGroupLimits,
}
glog.V(2).Info("Building the application source")
if err := builder.docker.BuildImage(opts); err != nil {
buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonDockerImageBuildFailed,
utilstatus.ReasonMessageDockerImageBuildFailed,
)
return buildResult, err
}
glog.V(2).Info("Cleaning up temporary containers")
builder.garbage.Cleanup(config)
var imageID string
var err error
if len(opts.Name) > 0 {
if imageID, err = builder.docker.GetImageID(opts.Name); err != nil {
buildResult.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return buildResult, err
}
}
return &api.Result{
Success: true,
WorkingDir: config.WorkingDir,
ImageID: imageID,
}, nil
}
// CreateDockerfile creates the ONBUILD Dockerfile
func (builder *OnBuild) CreateDockerfile(config *api.Config) error {
buffer := bytes.Buffer{}
uploadDir := filepath.Join(config.WorkingDir, "upload", "src")
buffer.WriteString(fmt.Sprintf("FROM %s\n", config.BuilderImage))
entrypoint, err := GuessEntrypoint(builder.fs, uploadDir)
if err != nil {
return err
}
env, err := scripts.GetEnvironment(filepath.Join(config.WorkingDir, constants.Source))
if err != nil {
glog.V(1).Infof("Environment: %v", err)
} else {
buffer.WriteString(scripts.ConvertEnvironmentToDocker(env))
}
// If there is an assemble script present, run it as part of the build process
// as the last thing.
if builder.hasAssembleScript(config) {
buffer.WriteString("RUN sh assemble\n")
}
// FIXME: This assumes that the WORKDIR is set to the application source root
// directory.
buffer.WriteString(fmt.Sprintf(`ENTRYPOINT ["./%s"]`+"\n", entrypoint))
return builder.fs.WriteFile(filepath.Join(uploadDir, "Dockerfile"), buffer.Bytes())
}
func (builder *OnBuild) copySTIScripts(config *api.Config) {
scriptsPath := filepath.Join(config.WorkingDir, "upload", "scripts")
sourcePath := filepath.Join(config.WorkingDir, "upload", "src")
if _, err := builder.fs.Stat(filepath.Join(scriptsPath, constants.Run)); err == nil {
glog.V(3).Info("Found S2I 'run' script, copying to application source dir")
builder.fs.Copy(filepath.Join(scriptsPath, constants.Run), filepath.Join(sourcePath, constants.Run), nil)
}
if _, err := builder.fs.Stat(filepath.Join(scriptsPath, constants.Assemble)); err == nil {
glog.V(3).Info("Found S2I 'assemble' script, copying to application source dir")
builder.fs.Copy(filepath.Join(scriptsPath, constants.Assemble), filepath.Join(sourcePath, constants.Assemble), nil)
}
}
// hasAssembleScript checks if the the assemble script is available
func (builder *OnBuild) hasAssembleScript(config *api.Config) bool {
assemblePath := filepath.Join(config.WorkingDir, "upload", "src", constants.Assemble)
_, err := builder.fs.Stat(assemblePath)
return err == nil
}
package sti
import (
"archive/tar"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/constants"
dockerpkg "github.com/openshift/source-to-image/pkg/docker"
s2ierr "github.com/openshift/source-to-image/pkg/errors"
s2itar "github.com/openshift/source-to-image/pkg/tar"
"github.com/openshift/source-to-image/pkg/util"
"github.com/openshift/source-to-image/pkg/util/fs"
utilstatus "github.com/openshift/source-to-image/pkg/util/status"
)
const maximumLabelSize = 10240
type postExecutorStepContext struct {
// id of the previous image that we're holding because after committing the image, we'll lose it.
// Used only when build is incremental and RemovePreviousImage setting is enabled.
// See also: storePreviousImageStep and removePreviousImageStep
previousImageID string
// Container id that will be committed.
// See also: commitImageStep
containerID string
// Path to a directory in the image where scripts (for example, "run") will be placed.
// This location will be used for generation of the CMD directive.
// See also: commitImageStep
destination string
// Image id created by committing the container.
// See also: commitImageStep and reportAboutSuccessStep
imageID string
// Labels that will be passed to a callback.
// These labels are added to the image during commit.
// See also: commitImageStep and STI.Build()
labels map[string]string
}
type postExecutorStep interface {
execute(*postExecutorStepContext) error
}
type storePreviousImageStep struct {
builder *STI
docker dockerpkg.Docker
}
func (step *storePreviousImageStep) execute(ctx *postExecutorStepContext) error {
if step.builder.incremental && step.builder.config.RemovePreviousImage {
glog.V(3).Info("Executing step: store previous image")
ctx.previousImageID = step.getPreviousImage()
return nil
}
glog.V(3).Info("Skipping step: store previous image")
return nil
}
func (step *storePreviousImageStep) getPreviousImage() string {
previousImageID, err := step.docker.GetImageID(step.builder.config.Tag)
if err != nil {
glog.V(0).Infof("error: Error retrieving previous image's (%v) metadata: %v", step.builder.config.Tag, err)
return ""
}
return previousImageID
}
type removePreviousImageStep struct {
builder *STI
docker dockerpkg.Docker
}
func (step *removePreviousImageStep) execute(ctx *postExecutorStepContext) error {
if step.builder.incremental && step.builder.config.RemovePreviousImage {
glog.V(3).Info("Executing step: remove previous image")
step.removePreviousImage(ctx.previousImageID)
return nil
}
glog.V(3).Info("Skipping step: remove previous image")
return nil
}
func (step *removePreviousImageStep) removePreviousImage(previousImageID string) {
if previousImageID == "" {
return
}
glog.V(1).Infof("Removing previously-tagged image %s", previousImageID)
if err := step.docker.RemoveImage(previousImageID); err != nil {
glog.V(0).Infof("error: Unable to remove previous image: %v", err)
}
}
type commitImageStep struct {
image string
builder *STI
docker dockerpkg.Docker
fs fs.FileSystem
tar s2itar.Tar
}
func (step *commitImageStep) execute(ctx *postExecutorStepContext) error {
glog.V(3).Infof("Executing step: commit image")
user, err := step.docker.GetImageUser(step.image)
if err != nil {
return fmt.Errorf("could not get user of %q image: %v", step.image, err)
}
cmd := createCommandForExecutingRunScript(step.builder.scriptsURL, ctx.destination)
if err = checkAndGetNewLabels(step.builder, step.docker, step.tar, ctx.containerID); err != nil {
return fmt.Errorf("could not check for new labels for %q image: %v", step.image, err)
}
ctx.labels = createLabelsForResultingImage(step.builder, step.docker, step.image)
if err = checkLabelSize(ctx.labels); err != nil {
return fmt.Errorf("label validation failed for %q image: %v", step.image, err)
}
// Set the image entrypoint back to its original value on commit, the running
// container has "env" as its entrypoint and we don't want to commit that.
entrypoint, err := step.docker.GetImageEntrypoint(step.image)
if err != nil {
return fmt.Errorf("could not get entrypoint of %q image: %v", step.image, err)
}
// If the image has no explicit entrypoint, set it to an empty array
// so we don't default to leaving the entrypoint as "env" upon commit.
if entrypoint == nil {
entrypoint = []string{}
}
startTime := time.Now()
ctx.imageID, err = commitContainer(
step.docker,
ctx.containerID,
cmd,
user,
step.builder.config.Tag,
step.builder.env,
entrypoint,
ctx.labels,
)
step.builder.result.BuildInfo.Stages = api.RecordStageAndStepInfo(step.builder.result.BuildInfo.Stages, api.StageCommit, api.StepCommitContainer, startTime, time.Now())
if err != nil {
step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonCommitContainerFailed,
utilstatus.ReasonMessageCommitContainerFailed,
)
return err
}
return nil
}
type downloadFilesFromBuilderImageStep struct {
builder *STI
docker dockerpkg.Docker
fs fs.FileSystem
tar s2itar.Tar
}
func (step *downloadFilesFromBuilderImageStep) execute(ctx *postExecutorStepContext) error {
glog.V(3).Info("Executing step: download files from the builder image")
artifactsDir := filepath.Join(step.builder.config.WorkingDir, constants.RuntimeArtifactsDir)
if err := step.fs.Mkdir(artifactsDir); err != nil {
step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonFSOperationFailed,
utilstatus.ReasonMessageFSOperationFailed,
)
return fmt.Errorf("could not create directory %q: %v", artifactsDir, err)
}
for _, artifact := range step.builder.config.RuntimeArtifacts {
if err := step.downloadAndExtractFile(artifact.Source, artifactsDir, ctx.containerID); err != nil {
step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonRuntimeArtifactsFetchFailed,
utilstatus.ReasonMessageRuntimeArtifactsFetchFailed,
)
return err
}
// for mapping like "/tmp/foo.txt -> app" we should create "app" and move "foo.txt" to that directory
dstSubDir := path.Clean(artifact.Destination)
if dstSubDir != "." && dstSubDir != "/" {
dstDir := filepath.Join(artifactsDir, dstSubDir)
glog.V(5).Infof("Creating directory %q", dstDir)
if err := step.fs.MkdirAll(dstDir); err != nil {
step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonFSOperationFailed,
utilstatus.ReasonMessageFSOperationFailed,
)
return fmt.Errorf("could not create directory %q: %v", dstDir, err)
}
currentFile := filepath.Base(artifact.Source)
oldFile := filepath.Join(artifactsDir, currentFile)
newFile := filepath.Join(artifactsDir, dstSubDir, currentFile)
glog.V(5).Infof("Renaming %q to %q", oldFile, newFile)
if err := step.fs.Rename(oldFile, newFile); err != nil {
step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonFSOperationFailed,
utilstatus.ReasonMessageFSOperationFailed,
)
return fmt.Errorf("could not rename %q -> %q: %v", oldFile, newFile, err)
}
}
}
return nil
}
func (step *downloadFilesFromBuilderImageStep) downloadAndExtractFile(artifactPath, artifactsDir, containerID string) error {
if res, err := downloadAndExtractFileFromContainer(step.docker, step.tar, artifactPath, artifactsDir, containerID); err != nil {
step.builder.result.BuildInfo.FailureReason = res
return err
}
return nil
}
type startRuntimeImageAndUploadFilesStep struct {
builder *STI
docker dockerpkg.Docker
fs fs.FileSystem
}
func (step *startRuntimeImageAndUploadFilesStep) execute(ctx *postExecutorStepContext) error {
glog.V(3).Info("Executing step: start runtime image and upload files")
fd, err := ioutil.TempFile("", "s2i-upload-done")
if err != nil {
step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return err
}
fd.Close()
lastFilePath := fd.Name()
defer func() {
os.Remove(lastFilePath)
}()
lastFileDstPath := "/tmp/" + filepath.Base(lastFilePath)
outReader, outWriter := io.Pipe()
errReader, errWriter := io.Pipe()
artifactsDir := filepath.Join(step.builder.config.WorkingDir, constants.RuntimeArtifactsDir)
// We copy scripts to a directory with artifacts to upload files in one shot
for _, script := range []string{constants.AssembleRuntime, constants.Run} {
// scripts must be inside of "scripts" subdir, see createCommandForExecutingRunScript()
destinationDir := filepath.Join(artifactsDir, "scripts")
err = step.copyScriptIfNeeded(script, destinationDir)
if err != nil {
step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return err
}
}
image := step.builder.config.RuntimeImage
workDir, err := step.docker.GetImageWorkdir(image)
if err != nil {
step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return fmt.Errorf("could not get working dir of %q image: %v", image, err)
}
commandBaseDir := filepath.Join(workDir, "scripts")
useExternalAssembleScript := step.builder.externalScripts[constants.AssembleRuntime]
if !useExternalAssembleScript {
// script already inside of the image
var scriptsURL string
scriptsURL, err = step.docker.GetScriptsURL(image)
if err != nil {
step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return err
}
if len(scriptsURL) == 0 {
step.builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return fmt.Errorf("could not determine scripts URL for image %q", image)
}
commandBaseDir = strings.TrimPrefix(scriptsURL, "image://")
}
cmd := fmt.Sprintf(
"while [ ! -f %q ]; do sleep 0.5; done; %s/%s; exit $?",
lastFileDstPath,
commandBaseDir,
constants.AssembleRuntime,
)
opts := dockerpkg.RunContainerOptions{
Image: image,
PullImage: false, // The PullImage is false because we've already pulled the image
CommandExplicit: []string{"/bin/sh", "-c", cmd},
Stdout: outWriter,
Stderr: errWriter,
NetworkMode: string(step.builder.config.DockerNetworkMode),
CGroupLimits: step.builder.config.CGroupLimits,
CapDrop: step.builder.config.DropCapabilities,
PostExec: step.builder.postExecutor,
Env: step.builder.env,
User: step.builder.config.AssembleRuntimeUser,
}
opts.OnStart = func(containerID string) error {
setStandardPerms := func(writer io.Writer) s2itar.Writer {
return s2itar.ChmodAdapter{Writer: tar.NewWriter(writer), NewFileMode: 0644, NewExecFileMode: 0755, NewDirMode: 0755}
}
glog.V(5).Infof("Uploading directory %q -> %q", artifactsDir, workDir)
onStartErr := step.docker.UploadToContainerWithTarWriter(step.fs, artifactsDir, workDir, containerID, setStandardPerms)
if onStartErr != nil {
return fmt.Errorf("could not upload directory (%q -> %q) into container %s: %v", artifactsDir, workDir, containerID, err)
}
glog.V(5).Infof("Uploading file %q -> %q", lastFilePath, lastFileDstPath)
onStartErr = step.docker.UploadToContainerWithTarWriter(step.fs, lastFilePath, lastFileDstPath, containerID, setStandardPerms)
if onStartErr != nil {
return fmt.Errorf("could not upload file (%q -> %q) into container %s: %v", lastFilePath, lastFileDstPath, containerID, err)
}
return onStartErr
}
dockerpkg.StreamContainerIO(outReader, nil, func(s string) { glog.V(0).Info(s) })
errOutput := ""
c := dockerpkg.StreamContainerIO(errReader, &errOutput, func(s string) { glog.Info(s) })
// switch to the next stage of post executors steps
step.builder.postExecutorStage++
err = step.docker.RunContainer(opts)
if e, ok := err.(s2ierr.ContainerError); ok {
// Must wait for StreamContainerIO goroutine above to exit before reading errOutput.
<-c
err = s2ierr.NewContainerError(image, e.ErrorCode, errOutput+e.Output)
}
return err
}
func (step *startRuntimeImageAndUploadFilesStep) copyScriptIfNeeded(script, destinationDir string) error {
useExternalScript := step.builder.externalScripts[script]
if useExternalScript {
src := filepath.Join(step.builder.config.WorkingDir, constants.UploadScripts, script)
dst := filepath.Join(destinationDir, script)
glog.V(5).Infof("Copying file %q -> %q", src, dst)
if err := step.fs.MkdirAll(destinationDir); err != nil {
return fmt.Errorf("could not create directory %q: %v", destinationDir, err)
}
if err := step.fs.Copy(src, dst, nil); err != nil {
return fmt.Errorf("could not copy file (%q -> %q): %v", src, dst, err)
}
}
return nil
}
type reportSuccessStep struct {
builder *STI
}
func (step *reportSuccessStep) execute(ctx *postExecutorStepContext) error {
glog.V(3).Info("Executing step: report success")
step.builder.result.Success = true
step.builder.result.ImageID = ctx.imageID
glog.V(3).Infof("Successfully built %s", util.FirstNonEmpty(step.builder.config.Tag, ctx.imageID))
return nil
}
// shared methods
func commitContainer(docker dockerpkg.Docker, containerID, cmd, user, tag string, env, entrypoint []string, labels map[string]string) (string, error) {
opts := dockerpkg.CommitContainerOptions{
Command: []string{cmd},
Env: env,
Entrypoint: entrypoint,
ContainerID: containerID,
Repository: tag,
User: user,
Labels: labels,
}
imageID, err := docker.CommitContainer(opts)
if err != nil {
return "", s2ierr.NewCommitError(tag, err)
}
return imageID, nil
}
func createLabelsForResultingImage(builder *STI, docker dockerpkg.Docker, baseImage string) map[string]string {
generatedLabels := util.GenerateOutputImageLabels(builder.sourceInfo, builder.config)
existingLabels, err := docker.GetLabels(baseImage)
if err != nil {
glog.V(0).Infof("error: Unable to read existing labels from the base image %s", baseImage)
}
configLabels := builder.config.Labels
newLabels := builder.newLabels
return mergeLabels(existingLabels, generatedLabels, configLabels, newLabels)
}
func mergeLabels(labels ...map[string]string) map[string]string {
mergedLabels := map[string]string{}
for _, labelMap := range labels {
for k, v := range labelMap {
mergedLabels[k] = v
}
}
return mergedLabels
}
func createCommandForExecutingRunScript(scriptsURL map[string]string, location string) string {
cmd := scriptsURL[constants.Run]
if strings.HasPrefix(cmd, "image://") {
// scripts from inside of the image, we need to strip the image part
// NOTE: We use path.Join instead of filepath.Join to avoid converting the
// path to UNC (Windows) format as we always run this inside container.
cmd = strings.TrimPrefix(cmd, "image://")
} else {
// external scripts, in which case we're taking the directory to which they
// were extracted and append scripts dir and name
cmd = path.Join(location, "scripts", constants.Run)
}
return cmd
}
func downloadAndExtractFileFromContainer(docker dockerpkg.Docker, tar s2itar.Tar, sourcePath, destinationPath, containerID string) (api.FailureReason, error) {
glog.V(5).Infof("Downloading file %q", sourcePath)
fd, err := ioutil.TempFile(destinationPath, "s2i-runtime-artifact")
if err != nil {
res := utilstatus.NewFailureReason(
utilstatus.ReasonFSOperationFailed,
utilstatus.ReasonMessageFSOperationFailed,
)
return res, fmt.Errorf("could not create temporary file for runtime artifact: %v", err)
}
defer func() {
fd.Close()
os.Remove(fd.Name())
}()
if err := docker.DownloadFromContainer(sourcePath, fd, containerID); err != nil {
res := utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return res, fmt.Errorf("could not download file (%q -> %q) from container %s: %v", sourcePath, fd.Name(), containerID, err)
}
// after writing to the file descriptor we need to rewind pointer to the beginning of the file before next reading
if _, err := fd.Seek(0, io.SeekStart); err != nil {
res := utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return res, fmt.Errorf("could not seek to the beginning of the file %q: %v", fd.Name(), err)
}
if err := tar.ExtractTarStream(destinationPath, fd); err != nil {
res := utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return res, fmt.Errorf("could not extract artifact %q into the directory %q: %v", sourcePath, destinationPath, err)
}
return utilstatus.NewFailureReason("", ""), nil
}
func checkLabelSize(labels map[string]string) error {
var sum = 0
for k, v := range labels {
sum += len(k) + len(v)
}
if sum > maximumLabelSize {
return fmt.Errorf("label size '%d' exceeds the maximum limit '%d'", sum, maximumLabelSize)
}
return nil
}
// check for new labels and apply to the output image.
func checkAndGetNewLabels(builder *STI, docker dockerpkg.Docker, tar s2itar.Tar, containerID string) error {
glog.V(3).Infof("Checking for new Labels to apply... ")
// metadata filename and its path inside the container
metadataFilename := "image_metadata.json"
sourceFilepath := filepath.Join("/tmp/.s2i", metadataFilename)
// create the 'downloadPath' folder if it doesn't exist
downloadPath := filepath.Join(builder.config.WorkingDir, "metadata")
glog.V(3).Infof("Creating the download path '%s'", downloadPath)
if err := os.MkdirAll(downloadPath, 0700); err != nil {
glog.Errorf("Error creating dir %q for '%s': %v", downloadPath, metadataFilename, err)
return err
}
// download & extract the file from container
if _, err := downloadAndExtractFileFromContainer(docker, tar, sourceFilepath, downloadPath, containerID); err != nil {
glog.V(3).Infof("unable to download and extract '%s' ... continuing", metadataFilename)
return nil
}
// open the file
filePath := filepath.Join(downloadPath, metadataFilename)
fd, err := os.Open(filePath)
if fd == nil || err != nil {
return fmt.Errorf("unable to open file '%s' : %v", downloadPath, err)
}
defer fd.Close()
// read the file to a string
str, err := ioutil.ReadAll(fd)
if err != nil {
return fmt.Errorf("error reading file '%s' in to a string: %v", filePath, err)
}
glog.V(3).Infof("new Labels File contents : \n%s\n", str)
// string into a map
var data map[string]interface{}
if err = json.Unmarshal([]byte(str), &data); err != nil {
return fmt.Errorf("JSON Unmarshal Error with '%s' file : %v", metadataFilename, err)
}
// update newLabels[]
labels := data["labels"]
for _, l := range labels.([]interface{}) {
for k, v := range l.(map[string]interface{}) {
builder.newLabels[k] = v.(string)
}
}
return nil
}
package sti
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/constants"
"github.com/openshift/source-to-image/pkg/build"
"github.com/openshift/source-to-image/pkg/build/strategies/layered"
dockerpkg "github.com/openshift/source-to-image/pkg/docker"
s2ierr "github.com/openshift/source-to-image/pkg/errors"
"github.com/openshift/source-to-image/pkg/ignore"
"github.com/openshift/source-to-image/pkg/scm"
"github.com/openshift/source-to-image/pkg/scm/git"
"github.com/openshift/source-to-image/pkg/scripts"
"github.com/openshift/source-to-image/pkg/tar"
"github.com/openshift/source-to-image/pkg/util"
"github.com/openshift/source-to-image/pkg/util/cmd"
"github.com/openshift/source-to-image/pkg/util/fs"
utilglog "github.com/openshift/source-to-image/pkg/util/glog"
utilstatus "github.com/openshift/source-to-image/pkg/util/status"
)
const (
injectionResultFile = "/tmp/injection-result"
rmInjectionsScript = "/tmp/rm-injections"
)
var (
glog = utilglog.StderrLog
// List of directories that needs to be present inside working dir
workingDirs = []string{
constants.UploadScripts,
constants.Source,
constants.DefaultScripts,
constants.UserScripts,
}
errMissingRequirements = errors.New("missing requirements")
)
// STI strategy executes the S2I build.
// For more details about S2I, visit https://github.com/openshift/source-to-image
type STI struct {
config *api.Config
result *api.Result
postExecutor dockerpkg.PostExecutor
installer scripts.Installer
runtimeInstaller scripts.Installer
git git.Git
fs fs.FileSystem
tar tar.Tar
docker dockerpkg.Docker
incrementalDocker dockerpkg.Docker
runtimeDocker dockerpkg.Docker
callbackInvoker util.CallbackInvoker
requiredScripts []string
optionalScripts []string
optionalRuntimeScripts []string
externalScripts map[string]bool
installedScripts map[string]bool
scriptsURL map[string]string
incremental bool
sourceInfo *git.SourceInfo
env []string
newLabels map[string]string
// Interfaces
preparer build.Preparer
ignorer build.Ignorer
artifacts build.IncrementalBuilder
scripts build.ScriptsHandler
source build.Downloader
garbage build.Cleaner
layered build.Builder
// post executors steps
postExecutorStage int
postExecutorFirstStageSteps []postExecutorStep
postExecutorSecondStageSteps []postExecutorStep
postExecutorStepsContext *postExecutorStepContext
}
// New returns the instance of STI builder strategy for the given config.
// If the layeredBuilder parameter is specified, then the builder provided will
// be used for the case that the base Docker image does not have 'tar' or 'bash'
// installed.
func New(client dockerpkg.Client, config *api.Config, fs fs.FileSystem, overrides build.Overrides) (*STI, error) {
excludePattern, err := regexp.Compile(config.ExcludeRegExp)
if err != nil {
return nil, err
}
docker := dockerpkg.New(client, config.PullAuthentication)
var incrementalDocker dockerpkg.Docker
if config.Incremental {
incrementalDocker = dockerpkg.New(client, config.IncrementalAuthentication)
}
inst := scripts.NewInstaller(
config.BuilderImage,
config.ScriptsURL,
config.ScriptDownloadProxyConfig,
docker,
config.PullAuthentication,
fs,
)
tarHandler := tar.NewParanoid(fs)
tarHandler.SetExclusionPattern(excludePattern)
builder := &STI{
installer: inst,
config: config,
docker: docker,
incrementalDocker: incrementalDocker,
git: git.New(fs, cmd.NewCommandRunner()),
fs: fs,
tar: tarHandler,
callbackInvoker: util.NewCallbackInvoker(),
requiredScripts: scripts.RequiredScripts,
optionalScripts: scripts.OptionalScripts,
optionalRuntimeScripts: []string{constants.AssembleRuntime},
externalScripts: map[string]bool{},
installedScripts: map[string]bool{},
scriptsURL: map[string]string{},
newLabels: map[string]string{},
}
if len(config.RuntimeImage) > 0 {
builder.runtimeDocker = dockerpkg.New(client, config.RuntimeAuthentication)
builder.runtimeInstaller = scripts.NewInstaller(
config.RuntimeImage,
config.ScriptsURL,
config.ScriptDownloadProxyConfig,
builder.runtimeDocker,
config.RuntimeAuthentication,
builder.fs,
)
}
// The sources are downloaded using the Git downloader.
// TODO: Add more SCM in future.
// TODO: explicit decision made to customize processing for usage specifically vs.
// leveraging overrides; also, we ultimately want to simplify s2i usage a good bit,
// which would lead to replacing this quick short circuit (so this change is tactical)
builder.source = overrides.Downloader
if builder.source == nil && !config.Usage {
downloader, err := scm.DownloaderForSource(builder.fs, config.Source, config.ForceCopy)
if err != nil {
return nil, err
}
builder.source = downloader
}
builder.garbage = build.NewDefaultCleaner(builder.fs, builder.docker)
builder.layered, err = layered.New(client, config, builder.fs, builder, overrides)
if err != nil {
return nil, err
}
// Set interfaces
builder.preparer = builder
// later on, if we support say .gitignore func in addition to .dockerignore
// func, setting ignorer will be based on config setting
builder.ignorer = &ignore.DockerIgnorer{}
builder.artifacts = builder
builder.scripts = builder
builder.postExecutor = builder
builder.initPostExecutorSteps()
return builder, nil
}
// Build processes a Request and returns a *api.Result and an error.
// An error represents a failure performing the build rather than a failure
// of the build itself. Callers should check the Success field of the result
// to determine whether a build succeeded or not.
func (builder *STI) Build(config *api.Config) (*api.Result, error) {
builder.result = &api.Result{}
if len(builder.config.CallbackURL) > 0 {
defer func() {
builder.result.Messages = builder.callbackInvoker.ExecuteCallback(
builder.config.CallbackURL,
builder.result.Success,
builder.postExecutorStepsContext.labels,
builder.result.Messages,
)
}()
}
defer builder.garbage.Cleanup(config)
glog.V(1).Infof("Preparing to build %s", config.Tag)
if err := builder.preparer.Prepare(config); err != nil {
return builder.result, err
}
if builder.incremental = builder.artifacts.Exists(config); builder.incremental {
tag := util.FirstNonEmpty(config.IncrementalFromTag, config.Tag)
glog.V(1).Infof("Existing image for tag %s detected for incremental build", tag)
} else {
glog.V(1).Info("Clean build will be performed")
}
glog.V(2).Infof("Performing source build from %s", config.Source)
if builder.incremental {
if err := builder.artifacts.Save(config); err != nil {
glog.Warning("Clean build will be performed because of error saving previous build artifacts")
glog.V(2).Infof("error: %v", err)
}
}
if len(config.AssembleUser) > 0 {
glog.V(1).Infof("Running %q in %q as %q user", constants.Assemble, config.Tag, config.AssembleUser)
} else {
glog.V(1).Infof("Running %q in %q", constants.Assemble, config.Tag)
}
startTime := time.Now()
if err := builder.scripts.Execute(constants.Assemble, config.AssembleUser, config); err != nil {
if err == errMissingRequirements {
glog.V(1).Info("Image is missing basic requirements (sh or tar), layered build will be performed")
return builder.layered.Build(config)
}
if e, ok := err.(s2ierr.ContainerError); ok {
if !isMissingRequirements(e.Output) {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonAssembleFailed,
utilstatus.ReasonMessageAssembleFailed,
)
return builder.result, err
}
glog.V(1).Info("Image is missing basic requirements (sh or tar), layered build will be performed")
buildResult, err := builder.layered.Build(config)
return buildResult, err
}
return builder.result, err
}
builder.result.BuildInfo.Stages = api.RecordStageAndStepInfo(builder.result.BuildInfo.Stages, api.StageAssemble, api.StepAssembleBuildScripts, startTime, time.Now())
builder.result.Success = true
return builder.result, nil
}
// Prepare prepares the source code and tar for build.
// NOTE: this func serves both the sti and onbuild strategies, as the OnBuild
// struct Build func leverages the STI struct Prepare func directly below.
func (builder *STI) Prepare(config *api.Config) error {
var err error
if builder.result == nil {
builder.result = &api.Result{}
}
if len(config.WorkingDir) == 0 {
if config.WorkingDir, err = builder.fs.CreateWorkingDirectory(); err != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonFSOperationFailed,
utilstatus.ReasonMessageFSOperationFailed,
)
return err
}
}
builder.result.WorkingDir = config.WorkingDir
if len(config.RuntimeImage) > 0 {
startTime := time.Now()
dockerpkg.GetRuntimeImage(builder.runtimeDocker, config)
builder.result.BuildInfo.Stages = api.RecordStageAndStepInfo(builder.result.BuildInfo.Stages, api.StagePullImages, api.StepPullRuntimeImage, startTime, time.Now())
if err != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonPullRuntimeImageFailed,
utilstatus.ReasonMessagePullRuntimeImageFailed,
)
glog.Errorf("Unable to pull runtime image %q: %v", config.RuntimeImage, err)
return err
}
// user didn't specify mapping, let's take it from the runtime image then
if len(builder.config.RuntimeArtifacts) == 0 {
var mapping string
mapping, err = builder.docker.GetAssembleInputFiles(config.RuntimeImage)
if err != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonInvalidArtifactsMapping,
utilstatus.ReasonMessageInvalidArtifactsMapping,
)
return err
}
if len(mapping) == 0 {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return errors.New("no runtime artifacts to copy were specified")
}
for _, value := range strings.Split(mapping, ";") {
if err = builder.config.RuntimeArtifacts.Set(value); err != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return fmt.Errorf("could not parse %q label with value %q on image %q: %v",
constants.AssembleInputFilesLabel, mapping, config.RuntimeImage, err)
}
}
}
if len(config.AssembleRuntimeUser) == 0 {
if config.AssembleRuntimeUser, err = builder.docker.GetAssembleRuntimeUser(config.RuntimeImage); err != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return fmt.Errorf("could not get %q label value on image %q: %v",
constants.AssembleRuntimeUserLabel, config.RuntimeImage, err)
}
}
// we're validating values here to be sure that we're handling both of the cases of the invocation:
// from main() and as a method from OpenShift
for _, volumeSpec := range builder.config.RuntimeArtifacts {
var volumeErr error
switch {
case !path.IsAbs(filepath.ToSlash(volumeSpec.Source)):
volumeErr = fmt.Errorf("invalid runtime artifacts mapping: %q -> %q: source must be an absolute path", volumeSpec.Source, volumeSpec.Destination)
case path.IsAbs(volumeSpec.Destination):
volumeErr = fmt.Errorf("invalid runtime artifacts mapping: %q -> %q: destination must be a relative path", volumeSpec.Source, volumeSpec.Destination)
case strings.HasPrefix(volumeSpec.Destination, ".."):
volumeErr = fmt.Errorf("invalid runtime artifacts mapping: %q -> %q: destination cannot start with '..'", volumeSpec.Source, volumeSpec.Destination)
default:
continue
}
if volumeErr != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonInvalidArtifactsMapping,
utilstatus.ReasonMessageInvalidArtifactsMapping,
)
return volumeErr
}
}
}
// Setup working directories
for _, v := range workingDirs {
if err = builder.fs.MkdirAllWithPermissions(filepath.Join(config.WorkingDir, v), 0755); err != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonFSOperationFailed,
utilstatus.ReasonMessageFSOperationFailed,
)
return err
}
}
// fetch sources, for their .s2i/bin might contain s2i scripts
if config.Source != nil {
if builder.sourceInfo, err = builder.source.Download(config); err != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonFetchSourceFailed,
utilstatus.ReasonMessageFetchSourceFailed,
)
return err
}
if config.SourceInfo != nil {
builder.sourceInfo = config.SourceInfo
}
}
// get the scripts
required, err := builder.installer.InstallRequired(builder.requiredScripts, config.WorkingDir)
if err != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonInstallScriptsFailed,
utilstatus.ReasonMessageInstallScriptsFailed,
)
return err
}
optional := builder.installer.InstallOptional(builder.optionalScripts, config.WorkingDir)
requiredAndOptional := append(required, optional...)
if len(config.RuntimeImage) > 0 && builder.runtimeInstaller != nil {
optionalRuntime := builder.runtimeInstaller.InstallOptional(builder.optionalRuntimeScripts, config.WorkingDir)
requiredAndOptional = append(requiredAndOptional, optionalRuntime...)
}
// If a ScriptsURL was specified, but no scripts were downloaded from it, throw an error
if len(config.ScriptsURL) > 0 {
failedCount := 0
for _, result := range requiredAndOptional {
if util.Includes(result.FailedSources, scripts.ScriptURLHandler) {
failedCount++
}
}
if failedCount == len(requiredAndOptional) {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonScriptsFetchFailed,
utilstatus.ReasonMessageScriptsFetchFailed,
)
return fmt.Errorf("could not download any scripts from URL %v", config.ScriptsURL)
}
}
for _, r := range requiredAndOptional {
if r.Error != nil {
glog.Warningf("Error getting %v from %s: %v", r.Script, r.URL, r.Error)
continue
}
builder.externalScripts[r.Script] = r.Downloaded
builder.installedScripts[r.Script] = r.Installed
builder.scriptsURL[r.Script] = r.URL
}
// see if there is a .s2iignore file, and if so, read in the patterns an then
// search and delete on
return builder.ignorer.Ignore(config)
}
// SetScripts allows to override default required and optional scripts
func (builder *STI) SetScripts(required, optional []string) {
builder.requiredScripts = required
builder.optionalScripts = optional
}
// PostExecute allows to execute post-build actions after the Docker
// container execution finishes.
func (builder *STI) PostExecute(containerID, destination string) error {
builder.postExecutorStepsContext.containerID = containerID
builder.postExecutorStepsContext.destination = destination
stageSteps := builder.postExecutorFirstStageSteps
if builder.postExecutorStage > 0 {
stageSteps = builder.postExecutorSecondStageSteps
}
for _, step := range stageSteps {
if err := step.execute(builder.postExecutorStepsContext); err != nil {
glog.V(0).Info("error: Execution of post execute step failed")
return err
}
}
return nil
}
// CreateBuildEnvironment constructs the environment variables to be provided to the assemble
// script and committed in the new image.
func CreateBuildEnvironment(sourcePath string, cfgEnv api.EnvironmentList) []string {
s2iEnv, err := scripts.GetEnvironment(filepath.Join(sourcePath, constants.Source))
if err != nil {
glog.V(3).Infof("No user environment provided (%v)", err)
}
return append(scripts.ConvertEnvironmentList(s2iEnv), scripts.ConvertEnvironmentList(cfgEnv)...)
}
// Exists determines if the current build supports incremental workflow.
// It checks if the previous image exists in the system and if so, then it
// verifies that the save-artifacts script is present.
func (builder *STI) Exists(config *api.Config) bool {
if !config.Incremental {
return false
}
policy := config.PreviousImagePullPolicy
if len(policy) == 0 {
policy = api.DefaultPreviousImagePullPolicy
}
tag := util.FirstNonEmpty(config.IncrementalFromTag, config.Tag)
startTime := time.Now()
result, err := dockerpkg.PullImage(tag, builder.incrementalDocker, policy)
builder.result.BuildInfo.Stages = api.RecordStageAndStepInfo(builder.result.BuildInfo.Stages, api.StagePullImages, api.StepPullPreviousImage, startTime, time.Now())
if err != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonPullPreviousImageFailed,
utilstatus.ReasonMessagePullPreviousImageFailed,
)
glog.V(2).Infof("Unable to pull previously built image %q: %v", tag, err)
return false
}
return result.Image != nil && builder.installedScripts[constants.SaveArtifacts]
}
// Save extracts and restores the build artifacts from the previous build to
// the current build.
func (builder *STI) Save(config *api.Config) (err error) {
artifactTmpDir := filepath.Join(config.WorkingDir, "upload", "artifacts")
if builder.result == nil {
builder.result = &api.Result{}
}
if err = builder.fs.Mkdir(artifactTmpDir); err != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonFSOperationFailed,
utilstatus.ReasonMessageFSOperationFailed,
)
return err
}
image := util.FirstNonEmpty(config.IncrementalFromTag, config.Tag)
outReader, outWriter := io.Pipe()
errReader, errWriter := io.Pipe()
glog.V(1).Infof("Saving build artifacts from image %s to path %s", image, artifactTmpDir)
extractFunc := func(string) error {
startTime := time.Now()
extractErr := builder.tar.ExtractTarStream(artifactTmpDir, outReader)
io.Copy(ioutil.Discard, outReader) // must ensure reader from container is drained
builder.result.BuildInfo.Stages = api.RecordStageAndStepInfo(builder.result.BuildInfo.Stages, api.StageRetrieve, api.StepRetrievePreviousArtifacts, startTime, time.Now())
if extractErr != nil {
builder.fs.RemoveDirectory(artifactTmpDir)
}
return extractErr
}
user := config.AssembleUser
if len(user) == 0 {
user, err = builder.docker.GetImageUser(image)
if err != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return err
}
glog.V(3).Infof("The assemble user is not set, defaulting to %q user", user)
} else {
glog.V(3).Infof("Using assemble user %q to extract artifacts", user)
}
opts := dockerpkg.RunContainerOptions{
Image: image,
User: user,
ExternalScripts: builder.externalScripts[constants.SaveArtifacts],
ScriptsURL: config.ScriptsURL,
Destination: config.Destination,
PullImage: false,
Command: constants.SaveArtifacts,
Stdout: outWriter,
Stderr: errWriter,
OnStart: extractFunc,
NetworkMode: string(config.DockerNetworkMode),
CGroupLimits: config.CGroupLimits,
CapDrop: config.DropCapabilities,
Binds: config.BuildVolumes,
SecurityOpt: config.SecurityOpt,
AddHost: config.AddHost,
}
dockerpkg.StreamContainerIO(errReader, nil, func(s string) { glog.Info(s) })
err = builder.docker.RunContainer(opts)
if e, ok := err.(s2ierr.ContainerError); ok {
err = s2ierr.NewSaveArtifactsError(image, e.Output, err)
}
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return err
}
// Execute runs the specified STI script in the builder image.
func (builder *STI) Execute(command string, user string, config *api.Config) error {
glog.V(2).Infof("Using image name %s", config.BuilderImage)
// Ensure that the builder image is present in the local Docker daemon.
// The image should have been pulled when the strategy was created, so
// this should be a quick inspect of the existing image. However, if
// the image has been deleted since the strategy was created, this will ensure
// it exists before executing a script on it.
builder.docker.CheckAndPullImage(config.BuilderImage)
// we can't invoke this method before (for example in New() method)
// because of later initialization of config.WorkingDir
builder.env = CreateBuildEnvironment(config.WorkingDir, config.Environment)
errOutput := ""
outReader, outWriter := io.Pipe()
errReader, errWriter := io.Pipe()
externalScripts := builder.externalScripts[command]
// if LayeredBuild is called then all the scripts will be placed inside the image
if config.LayeredBuild {
externalScripts = false
}
opts := dockerpkg.RunContainerOptions{
Image: config.BuilderImage,
Stdout: outWriter,
Stderr: errWriter,
// The PullImage is false because the PullImage function should be called
// before we run the container
PullImage: false,
ExternalScripts: externalScripts,
ScriptsURL: config.ScriptsURL,
Destination: config.Destination,
Command: command,
Env: builder.env,
User: user,
PostExec: builder.postExecutor,
NetworkMode: string(config.DockerNetworkMode),
CGroupLimits: config.CGroupLimits,
CapDrop: config.DropCapabilities,
Binds: config.BuildVolumes,
SecurityOpt: config.SecurityOpt,
AddHost: config.AddHost,
}
// If there are injections specified, override the original assemble script
// and wait till all injections are uploaded into the container that runs the
// assemble script.
injectionError := make(chan error)
if len(config.Injections) > 0 && command == constants.Assemble {
workdir, err := builder.docker.GetImageWorkdir(config.BuilderImage)
if err != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return err
}
config.Injections = util.FixInjectionsWithRelativePath(workdir, config.Injections)
truncatedFiles, err := util.ListFilesToTruncate(builder.fs, config.Injections)
if err != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonInstallScriptsFailed,
utilstatus.ReasonMessageInstallScriptsFailed,
)
return err
}
rmScript, err := util.CreateTruncateFilesScript(truncatedFiles, rmInjectionsScript)
if len(rmScript) != 0 {
defer os.Remove(rmScript)
}
if err != nil {
builder.result.BuildInfo.FailureReason = utilstatus.NewFailureReason(
utilstatus.ReasonGenericS2IBuildFailed,
utilstatus.ReasonMessageGenericS2iBuildFailed,
)
return err
}
opts.CommandOverrides = func(cmd string) string {
// If an s2i build has injections, the s2i container's main command must be altered to
// do the following:
// 1) Wait for the injections to be uploaded
// 2) Check if there were any errors uploading the injections
// 3) Run the injection removal script after `assemble` completes
//
// The injectionResultFile should always be uploaded to the s2i container after the
// injected volumes are added. If this file is non-empty, it indicates that an error
// occurred during the injection process and the s2i build should fail.
return fmt.Sprintf("while [ ! -f %[1]q ]; do sleep 0.5; done; if [ -s %[1]q ]; then exit 1; fi; %[2]s; result=$?; . %[3]s; exit $result",
injectionResultFile, cmd, rmInjectionsScript)
}
originalOnStart := opts.OnStart
opts.OnStart = func(containerID string) error {
defer close(injectionError)
injectErr := builder.uploadInjections(config, rmScript, containerID)
if err := builder.uploadInjectionResult(injectErr, containerID); err != nil {
injectionError <- err
return err
}
if originalOnStart != nil {
return originalOnStart(containerID)
}
return nil
}
} else {
close(injectionError)
}
if !config.LayeredBuild {
r, w := io.Pipe()
opts.Stdin = r
go func() {
// Wait for the injections to complete and check the error. Do not start
// streaming the sources when the injection failed.
if <-injectionError != nil {
w.Close()
return
}
glog.V(2).Info("starting the source uploading ...")
uploadDir := filepath.Join(config.WorkingDir, "upload")
w.CloseWithError(builder.tar.CreateTarStream(uploadDir, false, w))
}()
}
dockerpkg.StreamContainerIO(outReader, nil, func(s string) {
if !config.Quiet {
glog.Info(strings.TrimSpace(s))
}
})
c := dockerpkg.StreamContainerIO(errReader, &errOutput, func(s string) { glog.Info(s) })
err := builder.docker.RunContainer(opts)
if err != nil {
// Must wait for StreamContainerIO goroutine above to exit before reading errOutput.
<-c
if isMissingRequirements(errOutput) {
err = errMissingRequirements
} else if e, ok := err.(s2ierr.ContainerError); ok {
err = s2ierr.NewContainerError(config.BuilderImage, e.ErrorCode, errOutput+e.Output)
}
}
return err
}
// uploadInjections uploads the injected volumes to the s2i container, along with the source
// removal script to truncate volumes that should not be kept.
func (builder *STI) uploadInjections(config *api.Config, rmScript, containerID string) error {
glog.V(2).Info("starting the injections uploading ...")
for _, s := range config.Injections {
if err := builder.docker.UploadToContainer(builder.fs, s.Source, s.Destination, containerID); err != nil {
return util.HandleInjectionError(s, err)
}
}
if err := builder.docker.UploadToContainer(builder.fs, rmScript, rmInjectionsScript, containerID); err != nil {
return util.HandleInjectionError(api.VolumeSpec{Source: rmScript, Destination: rmInjectionsScript}, err)
}
return nil
}
func (builder *STI) initPostExecutorSteps() {
builder.postExecutorStepsContext = &postExecutorStepContext{}
if len(builder.config.RuntimeImage) == 0 {
builder.postExecutorFirstStageSteps = []postExecutorStep{
&storePreviousImageStep{
builder: builder,
docker: builder.docker,
},
&commitImageStep{
image: builder.config.BuilderImage,
builder: builder,
docker: builder.docker,
fs: builder.fs,
tar: builder.tar,
},
&reportSuccessStep{
builder: builder,
},
&removePreviousImageStep{
builder: builder,
docker: builder.docker,
},
}
} else {
builder.postExecutorFirstStageSteps = []postExecutorStep{
&downloadFilesFromBuilderImageStep{
builder: builder,
docker: builder.docker,
fs: builder.fs,
tar: builder.tar,
},
&startRuntimeImageAndUploadFilesStep{
builder: builder,
docker: builder.docker,
fs: builder.fs,
},
}
builder.postExecutorSecondStageSteps = []postExecutorStep{
&commitImageStep{
image: builder.config.RuntimeImage,
builder: builder,
docker: builder.docker,
tar: builder.tar,
},
&reportSuccessStep{
builder: builder,
},
}
}
}
// uploadInjectionResult uploads a result file to the s2i container, indicating
// that the injections have completed. If a non-nil error is passed in, it is returned
// to ensure the error status of the injection upload is reported.
func (builder *STI) uploadInjectionResult(startErr error, containerID string) error {
resultFile, err := util.CreateInjectionResultFile(startErr)
if len(resultFile) > 0 {
defer os.Remove(resultFile)
}
if err != nil {
return err
}
err = builder.docker.UploadToContainer(builder.fs, resultFile, injectionResultFile, containerID)
if err != nil {
return util.HandleInjectionError(api.VolumeSpec{Source: resultFile, Destination: injectionResultFile}, err)
}
return startErr
}
func isMissingRequirements(text string) bool {
tarCommand, _ := regexp.MatchString(`.*tar.*not found`, text)
shCommand, _ := regexp.MatchString(`.*/bin/sh.*no such file or directory`, text)
return tarCommand || shCommand
}
package sti
import (
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/constants"
"github.com/openshift/source-to-image/pkg/build"
"github.com/openshift/source-to-image/pkg/docker"
"github.com/openshift/source-to-image/pkg/util/fs"
)
// UsageHandler handles a config to display usage
type usageHandler interface {
build.ScriptsHandler
build.Preparer
SetScripts([]string, []string)
}
// Usage display usage information about a particular build image
type Usage struct {
handler usageHandler
garbage build.Cleaner
config *api.Config
}
// NewUsage creates a new instance of the default Usage implementation
func NewUsage(client docker.Client, config *api.Config) (*Usage, error) {
b, err := New(client, config, fs.NewFileSystem(), build.Overrides{})
if err != nil {
return nil, err
}
usage := Usage{
handler: b,
config: config,
garbage: b.garbage,
}
return &usage, nil
}
// Show starts the builder container and invokes the usage script on it
// to print usage information for the script.
func (u *Usage) Show() error {
b := u.handler
defer u.garbage.Cleanup(u.config)
b.SetScripts([]string{constants.Usage}, []string{})
if err := b.Prepare(u.config); err != nil {
return err
}
return b.Execute(constants.Usage, "", u.config)
}
package docker
import (
"archive/tar"
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net/http"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
dockertypes "github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
dockernetwork "github.com/docker/docker/api/types/network"
dockerapi "github.com/docker/docker/client"
dockermessage "github.com/docker/docker/pkg/jsonmessage"
dockerstdcopy "github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/tlsconfig"
"golang.org/x/net/context"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/constants"
s2ierr "github.com/openshift/source-to-image/pkg/errors"
s2itar "github.com/openshift/source-to-image/pkg/tar"
"github.com/openshift/source-to-image/pkg/util"
"github.com/openshift/source-to-image/pkg/util/fs"
"github.com/openshift/source-to-image/pkg/util/interrupt"
)
const (
// DefaultDestination is the destination where the artifacts will be placed
// if DestinationLabel was not specified.
DefaultDestination = "/tmp"
// DefaultTag is the image tag, being applied if none is specified.
DefaultTag = "latest"
// DefaultDockerTimeout specifies a timeout for Docker API calls. When this
// timeout is reached, certain Docker API calls might error out.
DefaultDockerTimeout = 2 * time.Minute
// DefaultShmSize is the default shared memory size to use (in bytes) if not specified.
DefaultShmSize = int64(1024 * 1024 * 64)
// DefaultPullRetryDelay is the default pull image retry interval
DefaultPullRetryDelay = 5 * time.Second
// DefaultPullRetryCount is the default pull image retry times
DefaultPullRetryCount = 6
)
var (
// RetriableErrors is a set of strings that indicate that an retriable error occurred.
RetriableErrors = []string{
"ping attempt failed with error",
"is already in progress",
"connection reset by peer",
"transport closed before response was received",
"connection refused",
}
)
// containerNamePrefix prefixes the name of containers launched by S2I. We
// cannot reuse the prefix "k8s" because we don't want the containers to be
// managed by a kubelet.
const containerNamePrefix = "s2i"
// containerName creates names for Docker containers launched by S2I. It is
// meant to resemble Kubernetes' pkg/kubelet/dockertools.BuildDockerName.
func containerName(image string) string {
//Initialize seed
rand.Seed(time.Now().UnixNano())
uid := fmt.Sprintf("%08x", rand.Uint32())
// Replace invalid characters for container name with underscores.
image = strings.Map(func(r rune) rune {
if ('0' <= r && r <= '9') || ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') {
return r
}
return '_'
}, image)
return fmt.Sprintf("%s_%s_%s", containerNamePrefix, image, uid)
}
// Docker is the interface between STI and the docker engine-api.
// It contains higher level operations called from the STI
// build or usage commands
type Docker interface {
IsImageInLocalRegistry(name string) (bool, error)
IsImageOnBuild(string) bool
GetOnBuild(string) ([]string, error)
RemoveContainer(id string) error
GetScriptsURL(name string) (string, error)
GetAssembleInputFiles(string) (string, error)
GetAssembleRuntimeUser(string) (string, error)
RunContainer(opts RunContainerOptions) error
GetImageID(name string) (string, error)
GetImageWorkdir(name string) (string, error)
CommitContainer(opts CommitContainerOptions) (string, error)
RemoveImage(name string) error
CheckImage(name string) (*api.Image, error)
PullImage(name string) (*api.Image, error)
CheckAndPullImage(name string) (*api.Image, error)
BuildImage(opts BuildImageOptions) error
GetImageUser(name string) (string, error)
GetImageEntrypoint(name string) ([]string, error)
GetLabels(name string) (map[string]string, error)
UploadToContainer(fs fs.FileSystem, srcPath, destPath, container string) error
UploadToContainerWithTarWriter(fs fs.FileSystem, srcPath, destPath, container string, makeTarWriter func(io.Writer) s2itar.Writer) error
DownloadFromContainer(containerPath string, w io.Writer, container string) error
Version() (dockertypes.Version, error)
CheckReachable() error
}
// Client contains all methods used when interacting directly with docker engine-api
type Client interface {
ContainerAttach(ctx context.Context, container string, options dockertypes.ContainerAttachOptions) (dockertypes.HijackedResponse, error)
ContainerCommit(ctx context.Context, container string, options dockertypes.ContainerCommitOptions) (dockertypes.IDResponse, error)
ContainerCreate(ctx context.Context, config *dockercontainer.Config, hostConfig *dockercontainer.HostConfig, networkingConfig *dockernetwork.NetworkingConfig, containerName string) (dockercontainer.ContainerCreateCreatedBody, error)
ContainerInspect(ctx context.Context, container string) (dockertypes.ContainerJSON, error)
ContainerRemove(ctx context.Context, container string, options dockertypes.ContainerRemoveOptions) error
ContainerStart(ctx context.Context, container string, options dockertypes.ContainerStartOptions) error
ContainerKill(ctx context.Context, container, signal string) error
ContainerWait(ctx context.Context, container string, condition dockercontainer.WaitCondition) (<-chan dockercontainer.ContainerWaitOKBody, <-chan error)
CopyToContainer(ctx context.Context, container, path string, content io.Reader, opts dockertypes.CopyToContainerOptions) error
CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, dockertypes.ContainerPathStat, error)
ImageBuild(ctx context.Context, buildContext io.Reader, options dockertypes.ImageBuildOptions) (dockertypes.ImageBuildResponse, error)
ImageInspectWithRaw(ctx context.Context, image string) (dockertypes.ImageInspect, []byte, error)
ImagePull(ctx context.Context, ref string, options dockertypes.ImagePullOptions) (io.ReadCloser, error)
ImageRemove(ctx context.Context, image string, options dockertypes.ImageRemoveOptions) ([]dockertypes.ImageDeleteResponseItem, error)
ServerVersion(ctx context.Context) (dockertypes.Version, error)
}
type stiDocker struct {
client Client
pullAuth dockertypes.AuthConfig
}
// InspectImage returns the image information and its raw representation.
func (d stiDocker) InspectImage(name string) (*dockertypes.ImageInspect, error) {
ctx, cancel := getDefaultContext()
defer cancel()
resp, _, err := d.client.ImageInspectWithRaw(ctx, name)
if err != nil {
return nil, err
}
return &resp, nil
}
// PostExecutor is an interface which provides a PostExecute function
type PostExecutor interface {
PostExecute(containerID, destination string) error
}
// PullResult is the result returned by the PullImage function
type PullResult struct {
OnBuild bool
Image *api.Image
}
// RunContainerOptions are options passed in to the RunContainer method
type RunContainerOptions struct {
Image string
PullImage bool
PullAuth api.AuthConfig
ExternalScripts bool
ScriptsURL string
Destination string
Env []string
AddHost []string
// Entrypoint will be used to override the default entrypoint
// for the image if it has one. If the image has no entrypoint,
// this value is ignored.
Entrypoint []string
Stdin io.ReadCloser
Stdout io.WriteCloser
Stderr io.WriteCloser
OnStart func(containerID string) error
PostExec PostExecutor
TargetImage bool
NetworkMode string
User string
CGroupLimits *api.CGroupLimits
CapDrop []string
Binds []string
Command string
CommandOverrides func(originalCmd string) string
// CommandExplicit provides a full control on the CMD directive.
// It won't modified in any way and will be passed to the docker as-is.
// Use this option when you want to use arbitrary command as CMD directive.
// In this case you can't use Command because 1) it's just a string
// 2) it will be modified by prepending base dir and cleaned by the path.Join().
// You also can't use CommandOverrides because 1) it's a string
// 2) it only gets applied when Command equals to "assemble" or "usage" script
// AND script is inside of the tar archive.
CommandExplicit []string
// SecurityOpt is passed through as security options to the underlying container.
SecurityOpt []string
}
// asDockerConfig converts a RunContainerOptions into a Config understood by the
// docker client
func (rco RunContainerOptions) asDockerConfig() dockercontainer.Config {
return dockercontainer.Config{
Image: getImageName(rco.Image),
User: rco.User,
Env: rco.Env,
Entrypoint: rco.Entrypoint,
OpenStdin: rco.Stdin != nil,
StdinOnce: rco.Stdin != nil,
AttachStdout: rco.Stdout != nil,
}
}
// asDockerHostConfig converts a RunContainerOptions into a HostConfig
// understood by the docker client
func (rco RunContainerOptions) asDockerHostConfig() dockercontainer.HostConfig {
hostConfig := dockercontainer.HostConfig{
CapDrop: rco.CapDrop,
PublishAllPorts: rco.TargetImage,
NetworkMode: dockercontainer.NetworkMode(rco.NetworkMode),
Binds: rco.Binds,
ExtraHosts: rco.AddHost,
SecurityOpt: rco.SecurityOpt,
}
if rco.CGroupLimits != nil {
hostConfig.Resources.Memory = rco.CGroupLimits.MemoryLimitBytes
hostConfig.Resources.MemorySwap = rco.CGroupLimits.MemorySwap
hostConfig.Resources.CgroupParent = rco.CGroupLimits.Parent
}
return hostConfig
}
// asDockerCreateContainerOptions converts a RunContainerOptions into a
// ContainerCreateConfig understood by the docker client
func (rco RunContainerOptions) asDockerCreateContainerOptions() dockertypes.ContainerCreateConfig {
config := rco.asDockerConfig()
hostConfig := rco.asDockerHostConfig()
return dockertypes.ContainerCreateConfig{
Name: containerName(rco.Image),
Config: &config,
HostConfig: &hostConfig,
}
}
// asDockerAttachToContainerOptions converts a RunContainerOptions into a
// ContainerAttachOptions understood by the docker client
func (rco RunContainerOptions) asDockerAttachToContainerOptions() dockertypes.ContainerAttachOptions {
return dockertypes.ContainerAttachOptions{
Stdin: rco.Stdin != nil,
Stdout: rco.Stdout != nil,
Stderr: rco.Stderr != nil,
Stream: rco.Stdout != nil,
}
}
// CommitContainerOptions are options passed in to the CommitContainer method
type CommitContainerOptions struct {
ContainerID string
Repository string
User string
Command []string
Env []string
Entrypoint []string
Labels map[string]string
}
// BuildImageOptions are options passed in to the BuildImage method
type BuildImageOptions struct {
Name string
Stdin io.Reader
Stdout io.WriteCloser
CGroupLimits *api.CGroupLimits
}
// NewEngineAPIClient creates a new Docker engine API client
func NewEngineAPIClient(config *api.DockerConfig) (*dockerapi.Client, error) {
var httpClient *http.Client
if config.UseTLS || config.TLSVerify {
tlscOptions := tlsconfig.Options{
InsecureSkipVerify: !config.TLSVerify,
}
if _, err := os.Stat(config.CAFile); !os.IsNotExist(err) {
tlscOptions.CAFile = config.CAFile
}
if _, err := os.Stat(config.CertFile); !os.IsNotExist(err) {
tlscOptions.CertFile = config.CertFile
}
if _, err := os.Stat(config.KeyFile); !os.IsNotExist(err) {
tlscOptions.KeyFile = config.KeyFile
}
tlsc, err := tlsconfig.Client(tlscOptions)
if err != nil {
return nil, err
}
httpClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsc,
},
}
}
return dockerapi.NewClient(config.Endpoint, os.Getenv("DOCKER_API_VERSION"), httpClient, nil)
}
// New creates a new implementation of the STI Docker interface
func New(client Client, auth api.AuthConfig) Docker {
return &stiDocker{
client: client,
pullAuth: dockertypes.AuthConfig{
Username: auth.Username,
Password: auth.Password,
Email: auth.Email,
ServerAddress: auth.ServerAddress,
},
}
}
func getDefaultContext() (context.Context, context.CancelFunc) {
// the intention is: all docker API calls with the exception of known long-
// running calls (ContainerWait, ImagePull, ImageBuild, ImageCommit) must complete within a
// certain timeout otherwise we bail.
return context.WithTimeout(context.Background(), DefaultDockerTimeout)
}
// GetImageWorkdir returns the WORKDIR property for the given image name.
// When the WORKDIR is not set or empty, return "/" instead.
func (d *stiDocker) GetImageWorkdir(name string) (string, error) {
resp, err := d.InspectImage(name)
if err != nil {
return "", err
}
workdir := resp.Config.WorkingDir
if len(workdir) == 0 {
// This is a default destination used by UploadToContainer when the WORKDIR
// is not set or it is empty. To show user where the injections will end up,
// we set this to "/".
workdir = "/"
}
return workdir, nil
}
// GetImageEntrypoint returns the ENTRYPOINT property for the given image name.
func (d *stiDocker) GetImageEntrypoint(name string) ([]string, error) {
image, err := d.InspectImage(name)
if err != nil {
return nil, err
}
return image.Config.Entrypoint, nil
}
// UploadToContainer uploads artifacts to the container.
func (d *stiDocker) UploadToContainer(fs fs.FileSystem, src, dest, container string) error {
makeWorldWritable := func(writer io.Writer) s2itar.Writer {
return s2itar.ChmodAdapter{Writer: tar.NewWriter(writer), NewFileMode: 0666, NewExecFileMode: 0666, NewDirMode: 0777}
}
return d.UploadToContainerWithTarWriter(fs, src, dest, container, makeWorldWritable)
}
// UploadToContainerWithTarWriter uploads artifacts to the container.
// If the source is a directory, then all files and sub-folders are copied into
// the destination (which has to be directory as well).
// If the source is a single file, then the file copied into destination (which
// has to be full path to a file inside the container).
func (d *stiDocker) UploadToContainerWithTarWriter(fs fs.FileSystem, src, dest, container string, makeTarWriter func(io.Writer) s2itar.Writer) error {
destPath := filepath.Dir(dest)
r, w := io.Pipe()
go func() {
tarWriter := makeTarWriter(w)
tarWriter = s2itar.RenameAdapter{Writer: tarWriter, Old: filepath.Base(src), New: filepath.Base(dest)}
err := s2itar.New(fs).CreateTarStreamToTarWriter(src, true, tarWriter, nil)
if err == nil {
err = tarWriter.Close()
}
w.CloseWithError(err)
}()
glog.V(3).Infof("Uploading %q to %q ...", src, destPath)
ctx, cancel := getDefaultContext()
defer cancel()
err := d.client.CopyToContainer(ctx, container, destPath, r, dockertypes.CopyToContainerOptions{})
if err != nil {
glog.V(0).Infof("error: Uploading to container failed: %v", err)
}
return err
}
// DownloadFromContainer downloads file (or directory) from the container.
func (d *stiDocker) DownloadFromContainer(containerPath string, w io.Writer, container string) error {
ctx, cancel := getDefaultContext()
defer cancel()
readCloser, _, err := d.client.CopyFromContainer(ctx, container, containerPath)
if err != nil {
return err
}
defer readCloser.Close()
_, err = io.Copy(w, readCloser)
return err
}
// IsImageInLocalRegistry determines whether the supplied image is in the local registry.
func (d *stiDocker) IsImageInLocalRegistry(name string) (bool, error) {
name = getImageName(name)
resp, err := d.InspectImage(name)
if resp != nil {
return true, nil
}
if err != nil && !dockerapi.IsErrNotFound(err) {
return false, s2ierr.NewInspectImageError(name, err)
}
return false, nil
}
// GetImageUser finds and retrieves the user associated with
// an image if one has been specified
func (d *stiDocker) GetImageUser(name string) (string, error) {
name = getImageName(name)
resp, err := d.InspectImage(name)
if err != nil {
glog.V(4).Infof("error inspecting image %s: %v", name, err)
return "", s2ierr.NewInspectImageError(name, err)
}
user := resp.Config.User
return user, nil
}
// Version returns information of the docker client and server host
func (d *stiDocker) Version() (dockertypes.Version, error) {
ctx, cancel := getDefaultContext()
defer cancel()
return d.client.ServerVersion(ctx)
}
// IsImageOnBuild provides information about whether the Docker image has
// OnBuild instruction recorded in the Image Config.
func (d *stiDocker) IsImageOnBuild(name string) bool {
onbuild, err := d.GetOnBuild(name)
return err == nil && len(onbuild) > 0
}
// GetOnBuild returns the set of ONBUILD Dockerfile commands to execute
// for the given image
func (d *stiDocker) GetOnBuild(name string) ([]string, error) {
name = getImageName(name)
resp, err := d.InspectImage(name)
if err != nil {
glog.V(4).Infof("error inspecting image %s: %v", name, err)
return nil, s2ierr.NewInspectImageError(name, err)
}
return resp.Config.OnBuild, nil
}
// CheckAndPullImage pulls an image into the local registry if not present
// and returns the image metadata
func (d *stiDocker) CheckAndPullImage(name string) (*api.Image, error) {
name = getImageName(name)
displayName := name
if !glog.Is(3) {
// For less verbose log levels (less than 3), shorten long iamge names like:
// "centos/php-56-centos7@sha256:51c3e2b08bd9fadefccd6ec42288680d6d7f861bdbfbd2d8d24960621e4e27f5"
// to include just enough characters to differentiate the build from others in the docker repository:
// "centos/php-56-centos7@sha256:51c3e2b08bd..."
// 18 characters is somewhat arbitrary, but should be enough to avoid a name collision.
split := strings.Split(name, "@")
if len(split) > 1 && len(split[1]) > 18 {
displayName = split[0] + "@" + split[1][:18] + "..."
}
}
image, err := d.CheckImage(name)
if err != nil && !strings.Contains(err.(s2ierr.Error).Details.Error(), "No such image") {
return nil, err
}
if image == nil {
glog.V(1).Infof("Image %q not available locally, pulling ...", displayName)
return d.PullImage(name)
}
glog.V(3).Infof("Using locally available image %q", displayName)
return image, nil
}
// CheckImage checks image from the local registry.
func (d *stiDocker) CheckImage(name string) (*api.Image, error) {
name = getImageName(name)
inspect, err := d.InspectImage(name)
if err != nil {
glog.V(4).Infof("error inspecting image %s: %v", name, err)
return nil, s2ierr.NewInspectImageError(name, err)
}
if inspect != nil {
image := &api.Image{}
updateImageWithInspect(image, inspect)
return image, nil
}
return nil, nil
}
func base64EncodeAuth(auth dockertypes.AuthConfig) (string, error) {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(auth); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(buf.Bytes()), nil
}
// PullImage pulls an image into the local registry
func (d *stiDocker) PullImage(name string) (*api.Image, error) {
name = getImageName(name)
// RegistryAuth is the base64 encoded credentials for the registry
base64Auth, err := base64EncodeAuth(d.pullAuth)
if err != nil {
return nil, s2ierr.NewPullImageError(name, err)
}
var retriableError = false
for retries := 0; retries <= DefaultPullRetryCount; retries++ {
err = util.TimeoutAfter(DefaultDockerTimeout, fmt.Sprintf("pulling image %q", name), func(timer *time.Timer) error {
resp, pullErr := d.client.ImagePull(context.Background(), name, dockertypes.ImagePullOptions{RegistryAuth: base64Auth})
if pullErr != nil {
return pullErr
}
defer resp.Close()
decoder := json.NewDecoder(resp)
for {
if !timer.Stop() {
return &util.TimeoutError{}
}
timer.Reset(DefaultDockerTimeout)
var msg dockermessage.JSONMessage
pullErr = decoder.Decode(&msg)
if pullErr == io.EOF {
return nil
}
if pullErr != nil {
return pullErr
}
if msg.Error != nil {
return msg.Error
}
if msg.Progress != nil {
glog.V(4).Infof("pulling image %s: %s", name, msg.Progress.String())
}
}
})
if err == nil {
break
}
glog.V(0).Infof("pulling image error : %v", err)
errMsg := fmt.Sprintf("%s", err)
for _, errorString := range RetriableErrors {
if strings.Contains(errMsg, errorString) {
retriableError = true
break
}
}
if !retriableError {
return nil, s2ierr.NewPullImageError(name, err)
}
glog.V(0).Infof("retrying in %s ...", DefaultPullRetryDelay)
time.Sleep(DefaultPullRetryDelay)
}
inspectResp, err := d.InspectImage(name)
if err != nil {
return nil, s2ierr.NewPullImageError(name, err)
}
if inspectResp != nil {
image := &api.Image{}
updateImageWithInspect(image, inspectResp)
return image, nil
}
return nil, nil
}
func updateImageWithInspect(image *api.Image, inspect *dockertypes.ImageInspect) {
image.ID = inspect.ID
if inspect.Config != nil {
image.Config = &api.ContainerConfig{
Labels: inspect.Config.Labels,
Env: inspect.Config.Env,
}
}
if inspect.ContainerConfig != nil {
image.ContainerConfig = &api.ContainerConfig{
Labels: inspect.ContainerConfig.Labels,
Env: inspect.ContainerConfig.Env,
}
}
}
// RemoveContainer removes a container and its associated volumes.
func (d *stiDocker) RemoveContainer(id string) error {
ctx, cancel := getDefaultContext()
defer cancel()
opts := dockertypes.ContainerRemoveOptions{
RemoveVolumes: true,
}
return d.client.ContainerRemove(ctx, id, opts)
}
// KillContainer kills a container.
func (d *stiDocker) KillContainer(id string) error {
ctx, cancel := getDefaultContext()
defer cancel()
return d.client.ContainerKill(ctx, id, "SIGKILL")
}
// GetLabels retrieves the labels of the given image.
func (d *stiDocker) GetLabels(name string) (map[string]string, error) {
name = getImageName(name)
resp, err := d.InspectImage(name)
if err != nil {
glog.V(4).Infof("error inspecting image %s: %v", name, err)
return nil, s2ierr.NewInspectImageError(name, err)
}
return resp.Config.Labels, nil
}
// getImageName checks the image name and adds DefaultTag if none is specified
func getImageName(name string) string {
_, tag, id := parseRepositoryTag(name)
if len(tag) == 0 && len(id) == 0 {
//_, tag, _ := parseRepositoryTag(name)
//if len(tag) == 0 {
return strings.Join([]string{name, DefaultTag}, ":")
}
return name
}
// getLabel gets label's value from the image metadata
func getLabel(image *api.Image, name string) string {
if value, ok := image.Config.Labels[name]; ok {
return value
}
return ""
}
// getVariable gets environment variable's value from the image metadata
func getVariable(image *api.Image, name string) string {
envName := name + "="
for _, v := range image.Config.Env {
if strings.HasPrefix(v, envName) {
return strings.TrimSpace(v[len(envName):])
}
}
return ""
}
// GetScriptsURL finds a scripts-url label on the given image.
func (d *stiDocker) GetScriptsURL(image string) (string, error) {
imageMetadata, err := d.CheckAndPullImage(image)
if err != nil {
return "", err
}
return getScriptsURL(imageMetadata), nil
}
// GetAssembleInputFiles finds a io.openshift.s2i.assemble-input-files label on the given image.
func (d *stiDocker) GetAssembleInputFiles(image string) (string, error) {
imageMetadata, err := d.CheckAndPullImage(image)
if err != nil {
return "", err
}
label := getLabel(imageMetadata, constants.AssembleInputFilesLabel)
if len(label) == 0 {
glog.V(0).Infof("warning: Image %q does not contain a value for the %s label", image, constants.AssembleInputFilesLabel)
} else {
glog.V(3).Infof("Image %q contains %s set to %q", image, constants.AssembleInputFilesLabel, label)
}
return label, nil
}
// GetAssembleRuntimeUser finds a io.openshift.s2i.assemble-runtime-user label on the given image.
func (d *stiDocker) GetAssembleRuntimeUser(image string) (string, error) {
imageMetadata, err := d.CheckAndPullImage(image)
if err != nil {
return "", err
}
return getLabel(imageMetadata, constants.AssembleRuntimeUserLabel), nil
}
// getScriptsURL finds a scripts url label in the image metadata
func getScriptsURL(image *api.Image) string {
if image == nil {
return ""
}
scriptsURL := getLabel(image, constants.ScriptsURLLabel)
// For backward compatibility, support the old label schema
if len(scriptsURL) == 0 {
scriptsURL = getLabel(image, constants.DeprecatedScriptsURLLabel)
if len(scriptsURL) > 0 {
glog.V(0).Infof("warning: Image %s uses deprecated label '%s', please migrate it to %s instead!",
image.ID, constants.DeprecatedScriptsURLLabel, constants.ScriptsURLLabel)
}
}
if len(scriptsURL) == 0 {
scriptsURL = getVariable(image, constants.ScriptsURLEnvironment)
if len(scriptsURL) != 0 {
glog.V(0).Infof("warning: Image %s uses deprecated environment variable %s, please migrate it to %s label instead!",
image.ID, constants.ScriptsURLEnvironment, constants.ScriptsURLLabel)
}
}
if len(scriptsURL) == 0 {
glog.V(0).Infof("warning: Image %s does not contain a value for the %s label", image.ID, constants.ScriptsURLLabel)
} else {
glog.V(2).Infof("Image %s contains %s set to %q", image.ID, constants.ScriptsURLLabel, scriptsURL)
}
return scriptsURL
}
// getDestination finds a destination label in the image metadata
func getDestination(image *api.Image) string {
if val := getLabel(image, constants.DestinationLabel); len(val) != 0 {
return val
}
// For backward compatibility, support the old label schema
if val := getLabel(image, constants.DeprecatedDestinationLabel); len(val) != 0 {
glog.V(0).Infof("warning: Image %s uses deprecated label '%s', please migrate it to %s instead!",
image.ID, constants.DeprecatedDestinationLabel, constants.DestinationLabel)
return val
}
if val := getVariable(image, constants.LocationEnvironment); len(val) != 0 {
glog.V(0).Infof("warning: Image %s uses deprecated environment variable %s, please migrate it to %s label instead!",
image.ID, constants.LocationEnvironment, constants.DestinationLabel)
return val
}
// default directory if none is specified
return DefaultDestination
}
func constructCommand(opts RunContainerOptions, imageMetadata *api.Image, tarDestination string) []string {
// base directory for all S2I commands
commandBaseDir := determineCommandBaseDir(opts, imageMetadata, tarDestination)
// NOTE: We use path.Join instead of filepath.Join to avoid converting the
// path to UNC (Windows) format as we always run this inside container.
binaryToRun := path.Join(commandBaseDir, opts.Command)
// when calling assemble script with Stdin parameter set (the tar file)
// we need to first untar the whole archive and only then call the assemble script
if opts.Stdin != nil && (opts.Command == constants.Assemble || opts.Command == constants.Usage) {
untarAndRun := fmt.Sprintf("tar -C %s -xf - && %s", tarDestination, binaryToRun)
resultedCommand := untarAndRun
if opts.CommandOverrides != nil {
resultedCommand = opts.CommandOverrides(untarAndRun)
}
return []string{"/bin/sh", "-c", resultedCommand}
}
return []string{binaryToRun}
}
func determineTarDestinationDir(opts RunContainerOptions, imageMetadata *api.Image) string {
if len(opts.Destination) != 0 {
return opts.Destination
}
return getDestination(imageMetadata)
}
func determineCommandBaseDir(opts RunContainerOptions, imageMetadata *api.Image, tarDestination string) string {
if opts.ExternalScripts {
// for external scripts we must always append 'scripts' because this is
// the default subdirectory inside tar for them
// NOTE: We use path.Join instead of filepath.Join to avoid converting the
// path to UNC (Windows) format as we always run this inside container.
glog.V(2).Infof("Both scripts and untarred source will be placed in '%s'", tarDestination)
return path.Join(tarDestination, "scripts")
}
// for internal scripts we can have separate path for scripts and untar operation destination
scriptsURL := opts.ScriptsURL
if len(scriptsURL) == 0 {
scriptsURL = getScriptsURL(imageMetadata)
}
commandBaseDir := strings.TrimPrefix(scriptsURL, "image://")
glog.V(2).Infof("Base directory for S2I scripts is '%s'. Untarring destination is '%s'.",
commandBaseDir, tarDestination)
return commandBaseDir
}
// dumpContainerInfo dumps information about a running container (port/IP/etc).
func dumpContainerInfo(container dockercontainer.ContainerCreateCreatedBody, d *stiDocker, image string) {
ctx, cancel := getDefaultContext()
defer cancel()
containerJSON, err := d.client.ContainerInspect(ctx, container.ID)
if err != nil {
return
}
liveports := "\n\nPort Bindings: "
for port, bindings := range containerJSON.NetworkSettings.NetworkSettingsBase.Ports {
liveports = liveports + "\n Container Port: " + string(port)
liveports = liveports + "\n Public Host / Port Mappings:"
for _, binding := range bindings {
liveports = liveports + "\n IP: " + binding.HostIP + " Port: " + binding.HostPort
}
}
liveports = liveports + "\n"
glog.V(0).Infof("\n\n\n\n\nThe image %s has been started in container %s as a result of the --run=true option. The container's stdout/stderr will be redirected to this command's glog output to help you validate its behavior. You can also inspect the container with docker commands if you like. If the container is set up to stay running, you will have to Ctrl-C to exit this command, which should also stop the container %s. This particular invocation attempts to run with the port mappings %+v \n\n\n\n\n", image, container.ID, container.ID, liveports)
}
// redirectResponseToOutputStream handles incoming streamed data from a
// container on a "hijacked" connection. If tty is true, expect multiplexed
// streams. Rules: 1) if you ask for streamed data from a container, you have
// to read it, otherwise sooner or later the container will block writing it.
// 2) if you're receiving multiplexed data, you have to actively read both
// streams in parallel, otherwise in the case of non-interleaved data, you, and
// then the container, will block.
func (d *stiDocker) redirectResponseToOutputStream(tty bool, outputStream, errorStream io.Writer, resp io.Reader) error {
if outputStream == nil {
outputStream = ioutil.Discard
}
if errorStream == nil {
errorStream = ioutil.Discard
}
var err error
if tty {
_, err = io.Copy(outputStream, resp)
} else {
_, err = dockerstdcopy.StdCopy(outputStream, errorStream, resp)
}
return err
}
// holdHijackedConnection pumps data up to the container's stdin, and runs a
// goroutine to pump data down from the container's stdout and stderr. it holds
// open the HijackedResponse until all of this is done. Caller's responsibility
// to close resp, as well as outputStream and errorStream if appropriate.
func (d *stiDocker) holdHijackedConnection(tty bool, opts *RunContainerOptions, resp dockertypes.HijackedResponse) error {
receiveStdout := make(chan error, 1)
if opts.Stdout != nil || opts.Stderr != nil {
go func() {
err := d.redirectResponseToOutputStream(tty, opts.Stdout, opts.Stderr, resp.Reader)
if opts.Stdout != nil {
opts.Stdout.Close()
opts.Stdout = nil
}
if opts.Stderr != nil {
opts.Stderr.Close()
opts.Stderr = nil
}
receiveStdout <- err
}()
} else {
receiveStdout <- nil
}
if opts.Stdin != nil {
_, err := io.Copy(resp.Conn, opts.Stdin)
opts.Stdin.Close()
opts.Stdin = nil
if err != nil {
<-receiveStdout
return err
}
}
err := resp.CloseWrite()
if err != nil {
<-receiveStdout
return err
}
// Hang around until the streaming is over - either when the server closes
// the connection, or someone locally closes resp.
return <-receiveStdout
}
// RunContainer creates and starts a container using the image specified in opts
// with the ability to stream input and/or output. Any non-nil
// opts.Std{in,out,err} will be closed upon return.
func (d *stiDocker) RunContainer(opts RunContainerOptions) error {
// Guarantee that Std{in,out,err} are closed upon return, including under
// error circumstances. In normal circumstances, holdHijackedConnection
// should do this for us.
defer func() {
if opts.Stdin != nil {
opts.Stdin.Close()
}
if opts.Stdout != nil {
opts.Stdout.Close()
}
if opts.Stderr != nil {
opts.Stderr.Close()
}
}()
createOpts := opts.asDockerCreateContainerOptions()
// get info about the specified image
image := createOpts.Config.Image
inspect, err := d.InspectImage(image)
imageMetadata := &api.Image{}
if err == nil {
updateImageWithInspect(imageMetadata, inspect)
if opts.PullImage {
_, err = d.CheckAndPullImage(image)
}
}
if err != nil {
glog.V(0).Infof("error: Unable to get image metadata for %s: %v", image, err)
return err
}
entrypoint, err := d.GetImageEntrypoint(image)
if err != nil {
return fmt.Errorf("could not get entrypoint of %q image: %v", image, err)
}
// If the image has an entrypoint already defined,
// it will be overridden either by DefaultEntrypoint,
// or by the value in opts.Entrypoint.
// If the image does not have an entrypoint, but
// opts.Entrypoint is supplied, opts.Entrypoint will
// be respected.
if len(entrypoint) != 0 && len(opts.Entrypoint) == 0 {
opts.Entrypoint = DefaultEntrypoint
}
// tarDestination will be passed as location to PostExecute function
// and will be used as the prefix for the CMD (scripts/run)
var tarDestination string
var cmd []string
if !opts.TargetImage {
if len(opts.CommandExplicit) != 0 {
cmd = opts.CommandExplicit
} else {
tarDestination = determineTarDestinationDir(opts, imageMetadata)
cmd = constructCommand(opts, imageMetadata, tarDestination)
}
glog.V(5).Infof("Setting %q command for container ...", strings.Join(cmd, " "))
}
createOpts.Config.Cmd = cmd
if createOpts.HostConfig != nil && createOpts.HostConfig.ShmSize <= 0 {
createOpts.HostConfig.ShmSize = DefaultShmSize
}
// Create a new container.
glog.V(2).Infof("Creating container with options {Name:%q Config:%+v HostConfig:%+v} ...", createOpts.Name, *util.SafeForLoggingContainerConfig(createOpts.Config), createOpts.HostConfig)
ctx, cancel := getDefaultContext()
defer cancel()
container, err := d.client.ContainerCreate(ctx, createOpts.Config, createOpts.HostConfig, createOpts.NetworkingConfig, createOpts.Name)
if err != nil {
return err
}
// Container was created, so we defer its removal, and also remove it if we get a SIGINT/SIGTERM/SIGQUIT/SIGHUP.
removeContainer := func() {
glog.V(4).Infof("Removing container %q ...", container.ID)
killErr := d.KillContainer(container.ID)
if removeErr := d.RemoveContainer(container.ID); removeErr != nil {
if killErr != nil {
glog.V(0).Infof("warning: Failed to kill container %q: %v", container.ID, killErr)
}
glog.V(0).Infof("warning: Failed to remove container %q: %v", container.ID, removeErr)
} else {
glog.V(4).Infof("Removed container %q", container.ID)
}
}
dumpStack := func(signal os.Signal) {
if signal == syscall.SIGQUIT {
buf := make([]byte, 1<<16)
runtime.Stack(buf, true)
fmt.Printf("%s", buf)
}
os.Exit(2)
}
return interrupt.New(dumpStack, removeContainer).Run(func() error {
glog.V(2).Infof("Attaching to container %q ...", container.ID)
ctx, cancel := getDefaultContext()
defer cancel()
resp, err := d.client.ContainerAttach(ctx, container.ID, opts.asDockerAttachToContainerOptions())
if err != nil {
glog.V(0).Infof("error: Unable to attach to container %q: %v", container.ID, err)
return err
}
defer resp.Close()
// Start the container
glog.V(2).Infof("Starting container %q ...", container.ID)
ctx, cancel = getDefaultContext()
defer cancel()
err = d.client.ContainerStart(ctx, container.ID, dockertypes.ContainerStartOptions{})
if err != nil {
return err
}
// Run OnStart hook if defined. OnStart might block, so we run it in a
// new goroutine, and wait for it to be done later on.
onStartDone := make(chan error, 1)
if opts.OnStart != nil {
go func() {
onStartDone <- opts.OnStart(container.ID)
}()
}
if opts.TargetImage {
// When TargetImage is true, we're dealing with an invocation of `s2i build ... --run`
// so this will, e.g., run a web server and block until the user interrupts it (or
// the container exits normally). dump port/etc information for the user.
dumpContainerInfo(container, d, image)
}
err = d.holdHijackedConnection(false, &opts, resp)
if err != nil {
return err
}
// Return an error if the exit code of the container is
// non-zero.
glog.V(4).Infof("Waiting for container %q to stop ...", container.ID)
waitC, errC := d.client.ContainerWait(context.Background(), container.ID, dockercontainer.WaitConditionNotRunning)
select {
case result := <-waitC:
if result.StatusCode != 0 {
var output string
jsonOutput, _ := d.client.ContainerInspect(ctx, container.ID)
if err == nil && jsonOutput.ContainerJSONBase != nil && jsonOutput.ContainerJSONBase.State != nil {
state := jsonOutput.ContainerJSONBase.State
output = fmt.Sprintf("Status: %s, Error: %s, OOMKilled: %v, Dead: %v", state.Status, state.Error, state.OOMKilled, state.Dead)
}
return s2ierr.NewContainerError(container.ID, int(result.StatusCode), output)
}
case err := <-errC:
return fmt.Errorf("waiting for container %q to stop: %v", container.ID, err)
}
// OnStart must be done before we move on.
if opts.OnStart != nil {
if err = <-onStartDone; err != nil {
return err
}
}
// Run PostExec hook if defined.
if opts.PostExec != nil {
glog.V(2).Infof("Invoking PostExecute function")
if err = opts.PostExec.PostExecute(container.ID, tarDestination); err != nil {
return err
}
}
return nil
})
}
// GetImageID retrieves the ID of the image identified by name
func (d *stiDocker) GetImageID(name string) (string, error) {
name = getImageName(name)
image, err := d.InspectImage(name)
if err != nil {
return "", err
}
return image.ID, nil
}
// CommitContainer commits a container to an image with a specific tag.
// The new image ID is returned
func (d *stiDocker) CommitContainer(opts CommitContainerOptions) (string, error) {
dockerOpts := dockertypes.ContainerCommitOptions{
Reference: opts.Repository,
}
if opts.Command != nil || opts.Entrypoint != nil {
config := dockercontainer.Config{
Cmd: opts.Command,
Entrypoint: opts.Entrypoint,
Env: opts.Env,
Labels: opts.Labels,
User: opts.User,
}
dockerOpts.Config = &config
glog.V(2).Infof("Committing container with dockerOpts: %+v, config: %+v", dockerOpts, *util.SafeForLoggingContainerConfig(&config))
}
resp, err := d.client.ContainerCommit(context.Background(), opts.ContainerID, dockerOpts)
if err == nil {
return resp.ID, nil
}
return "", err
}
// RemoveImage removes the image with specified ID
func (d *stiDocker) RemoveImage(imageID string) error {
ctx, cancel := getDefaultContext()
defer cancel()
_, err := d.client.ImageRemove(ctx, imageID, dockertypes.ImageRemoveOptions{})
return err
}
// BuildImage builds the image according to specified options
func (d *stiDocker) BuildImage(opts BuildImageOptions) error {
dockerOpts := dockertypes.ImageBuildOptions{
Tags: []string{opts.Name},
NoCache: true,
SuppressOutput: false,
Remove: true,
ForceRemove: true,
}
if opts.CGroupLimits != nil {
dockerOpts.Memory = opts.CGroupLimits.MemoryLimitBytes
dockerOpts.MemorySwap = opts.CGroupLimits.MemorySwap
dockerOpts.CgroupParent = opts.CGroupLimits.Parent
}
glog.V(2).Infof("Building container using config: %+v", dockerOpts)
resp, err := d.client.ImageBuild(context.Background(), opts.Stdin, dockerOpts)
if err != nil {
return err
}
defer resp.Body.Close()
// since can't pass in output stream to engine-api, need to copy contents of
// the output stream they create into our output stream
_, err = io.Copy(opts.Stdout, resp.Body)
if opts.Stdout != nil {
opts.Stdout.Close()
}
return err
}
package docker
import (
"errors"
"io"
"io/ioutil"
dockertypes "github.com/docker/docker/api/types"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/tar"
"github.com/openshift/source-to-image/pkg/util/fs"
)
// FakeDocker provides a fake docker interface
type FakeDocker struct {
LocalRegistryImage string
LocalRegistryResult bool
LocalRegistryError error
RemoveContainerID string
RemoveContainerError error
DefaultURLImage string
DefaultURLResult string
DefaultURLError error
AssembleInputFilesResult string
AssembleInputFilesError error
AssembleRuntimeUserResult string
AssembleRuntimeUserError error
RunContainerOpts RunContainerOptions
RunContainerError error
RunContainerErrorBeforeStart bool
RunContainerContainerID string
RunContainerCmd []string
GetImageIDImage string
GetImageIDResult string
GetImageIDError error
GetImageUserImage string
GetImageUserResult string
GetImageUserError error
GetImageEntrypointResult []string
GetImageEntrypointError error
CommitContainerOpts CommitContainerOptions
CommitContainerResult string
CommitContainerError error
RemoveImageName string
RemoveImageError error
BuildImageOpts BuildImageOptions
BuildImageError error
PullResult bool
PullError error
OnBuildImage string
OnBuildResult []string
OnBuildError error
IsOnBuildResult bool
IsOnBuildImage string
Labels map[string]string
LabelsError error
}
// IsImageInLocalRegistry checks if the image exists in the fake local registry
func (f *FakeDocker) IsImageInLocalRegistry(imageName string) (bool, error) {
f.LocalRegistryImage = imageName
return f.LocalRegistryResult, f.LocalRegistryError
}
// IsImageOnBuild returns true if the builder has onbuild instructions
func (f *FakeDocker) IsImageOnBuild(imageName string) bool {
f.IsOnBuildImage = imageName
return f.IsOnBuildResult
}
// Version returns information of the docker client and server host
func (f *FakeDocker) Version() (dockertypes.Version, error) {
return dockertypes.Version{}, nil
}
// GetImageWorkdir returns the workdir
func (f *FakeDocker) GetImageWorkdir(name string) (string, error) {
return "/", nil
}
// GetOnBuild returns the list of onbuild instructions for the given image
func (f *FakeDocker) GetOnBuild(imageName string) ([]string, error) {
f.OnBuildImage = imageName
return f.OnBuildResult, f.OnBuildError
}
// RemoveContainer removes a fake Docker container
func (f *FakeDocker) RemoveContainer(id string) error {
f.RemoveContainerID = id
return f.RemoveContainerError
}
// KillContainer kills a fake container
func (f *FakeDocker) KillContainer(id string) error {
return nil
}
// GetScriptsURL returns a default STI scripts URL
func (f *FakeDocker) GetScriptsURL(image string) (string, error) {
f.DefaultURLImage = image
return f.DefaultURLResult, f.DefaultURLError
}
// GetAssembleInputFiles finds a io.openshift.s2i.assemble-input-files label on the given image.
func (f *FakeDocker) GetAssembleInputFiles(image string) (string, error) {
return f.AssembleInputFilesResult, f.AssembleInputFilesError
}
// GetAssembleRuntimeUser finds a io.openshift.s2i.assemble-runtime-user label on the given image.
func (f *FakeDocker) GetAssembleRuntimeUser(image string) (string, error) {
return f.AssembleRuntimeUserResult, f.AssembleRuntimeUserError
}
// RunContainer runs a fake Docker container
func (f *FakeDocker) RunContainer(opts RunContainerOptions) error {
f.RunContainerOpts = opts
if f.RunContainerErrorBeforeStart {
return f.RunContainerError
}
if opts.Stdout != nil {
opts.Stdout.Close()
}
if opts.Stderr != nil {
opts.Stderr.Close()
}
if opts.OnStart != nil {
if err := opts.OnStart(""); err != nil {
return err
}
}
if opts.Stdin != nil {
_, err := io.Copy(ioutil.Discard, opts.Stdin)
if err != nil {
return err
}
}
if opts.PostExec != nil {
opts.PostExec.PostExecute(f.RunContainerContainerID, string(opts.Command))
}
return f.RunContainerError
}
// UploadToContainer uploads artifacts to the container.
func (f *FakeDocker) UploadToContainer(fs fs.FileSystem, srcPath, destPath, container string) error {
return nil
}
// UploadToContainerWithTarWriter uploads artifacts to the container.
func (f *FakeDocker) UploadToContainerWithTarWriter(fs fs.FileSystem, srcPath, destPath, container string, makeTarWriter func(io.Writer) tar.Writer) error {
return errors.New("not implemented")
}
// DownloadFromContainer downloads file (or directory) from the container.
func (f *FakeDocker) DownloadFromContainer(containerPath string, w io.Writer, container string) error {
return errors.New("not implemented")
}
// GetImageID returns a fake Docker image ID
func (f *FakeDocker) GetImageID(image string) (string, error) {
f.GetImageIDImage = image
return f.GetImageIDResult, f.GetImageIDError
}
// GetImageUser returns a fake user
func (f *FakeDocker) GetImageUser(image string) (string, error) {
f.GetImageUserImage = image
return f.GetImageUserResult, f.GetImageUserError
}
// GetImageEntrypoint returns an empty entrypoint
func (f *FakeDocker) GetImageEntrypoint(image string) ([]string, error) {
return f.GetImageEntrypointResult, f.GetImageEntrypointError
}
// CommitContainer commits a fake Docker container
func (f *FakeDocker) CommitContainer(opts CommitContainerOptions) (string, error) {
f.CommitContainerOpts = opts
return f.CommitContainerResult, f.CommitContainerError
}
// RemoveImage removes a fake Docker image
func (f *FakeDocker) RemoveImage(name string) error {
f.RemoveImageName = name
return f.RemoveImageError
}
// CheckImage checks image in local registry
func (f *FakeDocker) CheckImage(name string) (*api.Image, error) {
return nil, nil
}
// PullImage pulls a fake docker image
func (f *FakeDocker) PullImage(imageName string) (*api.Image, error) {
if f.PullResult {
return &api.Image{}, nil
}
return nil, f.PullError
}
// CheckAndPullImage pulls a fake docker image
func (f *FakeDocker) CheckAndPullImage(name string) (*api.Image, error) {
if f.PullResult {
return &api.Image{}, nil
}
return nil, f.PullError
}
// BuildImage builds image
func (f *FakeDocker) BuildImage(opts BuildImageOptions) error {
f.BuildImageOpts = opts
if opts.Stdin != nil {
_, err := io.Copy(ioutil.Discard, opts.Stdin)
if err != nil {
return err
}
}
return f.BuildImageError
}
// GetLabels returns the labels of the image
func (f *FakeDocker) GetLabels(name string) (map[string]string, error) {
return f.Labels, f.LabelsError
}
// CheckReachable returns if the Docker daemon is reachable from s2i
func (f *FakeDocker) CheckReachable() error {
return nil
}
package docker
import (
"bufio"
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/docker/distribution/reference"
cliconfig "github.com/docker/docker/cli/config"
"github.com/docker/docker/client"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/constants"
s2ierr "github.com/openshift/source-to-image/pkg/errors"
utilglog "github.com/openshift/source-to-image/pkg/util/glog"
"github.com/openshift/source-to-image/pkg/util/user"
)
var (
// glog is a placeholder until the builders pass an output stream down client
// facing libraries should not be using glog
glog = utilglog.StderrLog
// DefaultEntrypoint is the default entry point used when starting containers
DefaultEntrypoint = []string{"/usr/bin/env"}
)
// AuthConfigurations maps a registry name to an AuthConfig, as used for
// example in the .dockercfg file
type AuthConfigurations struct {
Configs map[string]api.AuthConfig
}
type dockerConfig struct {
Auth string `json:"auth"`
Email string `json:"email"`
}
const (
// maxErrorOutput is the maximum length of the error output saved for
// processing
maxErrorOutput = 1024
defaultRegistry = "https://index.docker.io/v1/"
)
// GetImageRegistryAuth retrieves the appropriate docker client authentication
// object for a given image name and a given set of client authentication
// objects.
func GetImageRegistryAuth(auths *AuthConfigurations, imageName string) api.AuthConfig {
glog.V(5).Infof("Getting docker credentials for %s", imageName)
if auths == nil {
return api.AuthConfig{}
}
ref, err := parseNamedDockerImageReference(imageName)
if err != nil {
glog.V(0).Infof("error: Failed to parse docker reference %s", imageName)
return api.AuthConfig{}
}
if ref.Registry != "" {
if auth, ok := auths.Configs[ref.Registry]; ok {
glog.V(5).Infof("Using %s[%s] credentials for pulling %s", auth.Email, ref.Registry, imageName)
return auth
}
}
if auth, ok := auths.Configs[defaultRegistry]; ok {
glog.V(5).Infof("Using %s credentials for pulling %s", auth.Email, imageName)
return auth
}
return api.AuthConfig{}
}
// namedDockerImageReference points to a Docker image.
type namedDockerImageReference struct {
Registry string
Namespace string
Name string
Tag string
ID string
}
// parseNamedDockerImageReference parses a Docker pull spec string into a
// NamedDockerImageReference.
func parseNamedDockerImageReference(spec string) (namedDockerImageReference, error) {
var ref namedDockerImageReference
namedRef, err := reference.ParseNormalizedNamed(spec)
if err != nil {
return ref, err
}
name := namedRef.Name()
i := strings.IndexRune(name, '/')
if i == -1 || (!strings.ContainsAny(name[:i], ":.") && name[:i] != "localhost") {
ref.Name = name
} else {
ref.Registry, ref.Name = name[:i], name[i+1:]
}
if named, ok := namedRef.(reference.NamedTagged); ok {
ref.Tag = named.Tag()
}
if named, ok := namedRef.(reference.Canonical); ok {
ref.ID = named.Digest().String()
}
// It's not enough just to use the reference.ParseNamed(). We have to fill
// ref.Namespace from ref.Name
if i := strings.IndexRune(ref.Name, '/'); i != -1 {
ref.Namespace, ref.Name = ref.Name[:i], ref.Name[i+1:]
}
return ref, nil
}
// LoadImageRegistryAuth loads and returns the set of client auth objects from
// a docker config json file.
func LoadImageRegistryAuth(dockerCfg io.Reader) *AuthConfigurations {
auths, err := NewAuthConfigurations(dockerCfg)
if err != nil {
glog.V(0).Infof("error: Unable to load docker config: %v", err)
return nil
}
return auths
}
// begin next 3 methods borrowed from go-dockerclient
// NewAuthConfigurations finishes creating the auth config array s2i pulls from
// any auth config file it is pointed to when started from the command line
func NewAuthConfigurations(r io.Reader) (*AuthConfigurations, error) {
var auth *AuthConfigurations
confs, err := parseDockerConfig(r)
if err != nil {
return nil, err
}
auth, err = authConfigs(confs)
if err != nil {
return nil, err
}
return auth, nil
}
// parseDockerConfig does the json unmarshalling of the data we read from the
// file
func parseDockerConfig(r io.Reader) (map[string]dockerConfig, error) {
buf := new(bytes.Buffer)
buf.ReadFrom(r)
byteData := buf.Bytes()
confsWrapper := struct {
Auths map[string]dockerConfig `json:"auths"`
}{}
if err := json.Unmarshal(byteData, &confsWrapper); err == nil {
if len(confsWrapper.Auths) > 0 {
return confsWrapper.Auths, nil
}
}
var confs map[string]dockerConfig
if err := json.Unmarshal(byteData, &confs); err != nil {
return nil, err
}
return confs, nil
}
// authConfigs converts a dockerConfigs map to a AuthConfigurations object.
func authConfigs(confs map[string]dockerConfig) (*AuthConfigurations, error) {
c := &AuthConfigurations{
Configs: make(map[string]api.AuthConfig),
}
for reg, conf := range confs {
if len(conf.Auth) == 0 {
continue
}
data, err := base64.StdEncoding.DecodeString(conf.Auth)
if err != nil {
return nil, err
}
userpass := strings.SplitN(string(data), ":", 2)
if len(userpass) != 2 {
return nil, fmt.Errorf("cannot parse username/password from %s", userpass)
}
c.Configs[reg] = api.AuthConfig{
Email: conf.Email,
Username: userpass[0],
Password: userpass[1],
ServerAddress: reg,
}
}
return c, nil
}
// end block of 3 methods borrowed from go-dockerclient
// StreamContainerIO starts a goroutine to take data from the reader and
// redirect it to the log function (typically we pass in glog.Error for stderr
// and glog.Info for stdout. The caller should wrap glog functions in a closure
// to ensure accurate line numbers are reported:
// https://github.com/openshift/source-to-image/issues/558 .
// StreamContainerIO returns a channel which is closed after the reader is
// closed.
func StreamContainerIO(r io.Reader, errOutput *string, log func(string)) <-chan struct{} {
c := make(chan struct{}, 1)
go func() {
reader := bufio.NewReader(r)
for {
text, err := reader.ReadString('\n')
if text != "" {
log(text)
}
if errOutput != nil && len(*errOutput) < maxErrorOutput {
*errOutput += text + "\n"
}
if err != nil {
if glog.Is(2) && err != io.EOF {
glog.V(0).Infof("error: Error reading docker stdout/stderr: %#v", err)
}
break
}
}
close(c)
}()
return c
}
// TODO remove (base, tag, id)
func parseRepositoryTag(repos string) (string, string, string) {
n := strings.Index(repos, "@")
if n >= 0 {
parts := strings.Split(repos, "@")
return parts[0], "", parts[1]
}
n = strings.LastIndex(repos, ":")
if n < 0 {
return repos, "", ""
}
if tag := repos[n+1:]; !strings.Contains(tag, "/") {
return repos[:n], tag, ""
}
return repos, "", ""
}
// PullImage pulls the Docker image specified by name taking the pull policy
// into the account.
func PullImage(name string, d Docker, policy api.PullPolicy) (*PullResult, error) {
if len(policy) == 0 {
return nil, errors.New("the policy for pull image must be set")
}
var (
image *api.Image
err error
)
switch policy {
case api.PullIfNotPresent:
image, err = d.CheckAndPullImage(name)
case api.PullAlways:
glog.Infof("Pulling image %q ...", name)
image, err = d.PullImage(name)
case api.PullNever:
glog.Infof("Checking if image %q is available locally ...", name)
image, err = d.CheckImage(name)
}
return &PullResult{Image: image, OnBuild: d.IsImageOnBuild(name)}, err
}
// CheckAllowedUser retrieves the execution users for a Docker image and
// checks that user against an allowed range of uids.
// - If the range of users is not empty, then the user on the Docker image
// needs to be a numeric user
// - The user's uid must be contained by the range(s) specified by the uids
// Rangelist
// - If build image uses an assemble user (via a command override or an
// image label), that user must be within the allowed range of uids.
// - If the image contains ONBUILD instructions and those instructions also
// contain any USER directives, then all users specified by those USER directives
// must meet the uid range criteria as well.
func CheckAllowedUser(d Docker, imageName string, uids user.RangeList, isOnbuild bool, assembleUserConfig string) error {
if uids == nil || uids.Empty() {
return nil
}
// OnBuild users always need to be checked for layered builds
// Only return error if a user is not allowed, otherwise continue
if isOnbuild {
onBuildUsers, err := extractOnBuildUsers(d, imageName)
if err != nil {
return err
}
for _, usr := range onBuildUsers {
if !user.IsUserAllowed(usr, &uids) {
return s2ierr.NewUserNotAllowedError(imageName, true)
}
}
}
// P1: Assemble user configuration
if len(assembleUserConfig) > 0 {
if !user.IsUserAllowed(assembleUserConfig, &uids) {
// Pass in the override, since assembleUser can come from the image label
return s2ierr.NewAssembleUserNotAllowedError(imageName, true)
}
return nil
}
// P2: Assemble user label in image
assembleUser, err := extractAssembleUser(d, imageName)
if err != nil {
return err
}
if len(assembleUser) > 0 {
if !user.IsUserAllowed(assembleUser, &uids) {
// Pass in the override, since assembleUser can come from the image label
return s2ierr.NewAssembleUserNotAllowedError(imageName, false)
}
return nil
}
// Default - image user
imageUser, err := extractImageUser(d, imageName)
if err != nil {
return err
}
if !user.IsUserAllowed(imageUser, &uids) {
return s2ierr.NewUserNotAllowedError(imageName, false)
}
return nil
}
func extractUser(userSpec string) string {
if strings.Contains(userSpec, ":") {
parts := strings.SplitN(userSpec, ":", 2)
return strings.TrimSpace(parts[0])
}
return strings.TrimSpace(userSpec)
}
func extractImageUser(d Docker, imageName string) (string, error) {
imageUserSpec, err := d.GetImageUser(imageName)
if err != nil {
return "", err
}
imageUser := extractUser(imageUserSpec)
return imageUser, nil
}
var dockerLineDelim = regexp.MustCompile(`[\t\v\f\r ]+`)
// extractOnBuildUsers checks a list of Docker ONBUILD instructions for user
// directives. It returns a list of users specified in the ONBUILD directives.
func extractOnBuildUsers(d Docker, imageName string) ([]string, error) {
cmds, err := d.GetOnBuild(imageName)
var users []string
if err != nil {
return users, err
}
for _, line := range cmds {
parts := dockerLineDelim.Split(line, 2)
if strings.ToLower(parts[0]) != "user" {
continue
}
users = append(users, extractUser(parts[1]))
}
return users, nil
}
// CheckReachable returns if the Docker daemon is reachable from s2i
func (d *stiDocker) CheckReachable() error {
_, err := d.Version()
return err
}
func pullAndCheck(image string, docker Docker, pullPolicy api.PullPolicy, config *api.Config) (*PullResult, error) {
r, err := PullImage(image, docker, pullPolicy)
if err != nil {
return nil, err
}
err = CheckAllowedUser(docker, image, config.AllowedUIDs, r.OnBuild, config.AssembleUser)
if err != nil {
return nil, err
}
return r, nil
}
// GetBuilderImage processes the config and performs operations necessary to
// make the Docker image specified as BuilderImage available locally. It
// returns information about the base image, containing metadata necessary for
// choosing the right STI build strategy.
func GetBuilderImage(docker Docker, config *api.Config) (*PullResult, error) {
return pullAndCheck(config.BuilderImage, docker, config.BuilderPullPolicy, config)
}
// GetRebuildImage obtains the metadata information for the image specified in
// a s2i rebuild operation. Assumptions are made that the build is available
// locally since it should have been previously built.
func GetRebuildImage(docker Docker, config *api.Config) (*PullResult, error) {
return pullAndCheck(config.Tag, docker, config.BuilderPullPolicy, config)
}
// GetRuntimeImage processes the config and performs operations necessary to
// make the Docker image specified as RuntimeImage available locally.
func GetRuntimeImage(docker Docker, config *api.Config) error {
_, err := pullAndCheck(config.RuntimeImage, docker, config.RuntimeImagePullPolicy, config)
return err
}
// GetDefaultDockerConfig checks relevant Docker environment variables to
// provide defaults for our command line flags
func GetDefaultDockerConfig() *api.DockerConfig {
cfg := &api.DockerConfig{}
if cfg.Endpoint = os.Getenv("DOCKER_HOST"); cfg.Endpoint == "" {
cfg.Endpoint = client.DefaultDockerHost
}
certPath := os.Getenv("DOCKER_CERT_PATH")
if certPath == "" {
certPath = cliconfig.Dir()
}
cfg.CertFile = filepath.Join(certPath, "cert.pem")
cfg.KeyFile = filepath.Join(certPath, "key.pem")
cfg.CAFile = filepath.Join(certPath, "ca.pem")
if tlsVerify := os.Getenv("DOCKER_TLS_VERIFY"); tlsVerify != "" {
cfg.TLSVerify = true
}
return cfg
}
// GetAssembleUser finds an assemble user on the given image.
// This functions receives the config to check if the AssembleUser was defined in command line
// If the cmd is blank, it tries to fetch the value from the Builder Image defined Label (assemble-user)
// Otherwise it follows the common flow, using the USER defined in Dockerfile
func GetAssembleUser(docker Docker, config *api.Config) (string, error) {
if len(config.AssembleUser) > 0 {
return config.AssembleUser, nil
}
return extractAssembleUser(docker, config.BuilderImage)
}
func extractAssembleUser(docker Docker, imageName string) (string, error) {
imageData, err := docker.GetLabels(imageName)
if err != nil {
return "", err
}
return imageData[constants.AssembleUserLabel], nil
}
package ignore
import (
"bufio"
"io"
"os"
"path/filepath"
"strings"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/constants"
utilglog "github.com/openshift/source-to-image/pkg/util/glog"
)
var glog = utilglog.StderrLog
// DockerIgnorer ignores files based on the contents of the .s2iignore file
type DockerIgnorer struct{}
// Ignore removes files from the workspace based on the contents of the
// .s2iignore file
func (b *DockerIgnorer) Ignore(config *api.Config) error {
/*
so, to duplicate the .dockerignore capabilities (https://docs.docker.com/reference/builder/#dockerignore-file)
we have a flow that follows:
0) First note, .dockerignore rules are NOT recursive (unlike .gitignore) .. you have to list subdir explicitly
1) Read in the exclusion patterns
2) Skip over comments (noted by #)
3) note overrides (via exclamation sign i.e. !) and reinstate files (don't remove) as needed
4) leverage Glob matching to build list, as .dockerignore is documented as following filepath.Match / filepath.Glob
5) del files
1 to 4 is in getListOfFilesToIgnore
*/
filesToDel, lerr := b.GetListOfFilesToIgnore(config.WorkingSourceDir)
if lerr != nil {
return lerr
}
if filesToDel == nil {
return nil
}
// delete compiled list of files
for _, fileToDel := range filesToDel {
glog.V(5).Infof("attempting to remove file %s \n", fileToDel)
rerr := os.RemoveAll(fileToDel)
if rerr != nil {
glog.Errorf("error removing file %s because of %v \n", fileToDel, rerr)
return rerr
}
}
return nil
}
// GetListOfFilesToIgnore returns list of files from the workspace based on the contents of the
// .s2iignore file
func (b *DockerIgnorer) GetListOfFilesToIgnore(workingDir string) (map[string]string, error) {
path := filepath.Join(workingDir, constants.IgnoreFile)
file, err := os.Open(path)
if err != nil {
if !os.IsNotExist(err) {
glog.Errorf("Ignore processing, problem opening %s because of %v\n", path, err)
return nil, err
}
glog.V(4).Info(".s2iignore file does not exist")
return nil, nil
}
defer file.Close()
filesToDel := make(map[string]string)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
filespec := strings.Trim(scanner.Text(), " ")
if len(filespec) == 0 {
continue
}
if strings.HasPrefix(filespec, "#") {
continue
}
glog.V(4).Infof(".s2iignore lists a file spec of %s \n", filespec)
if strings.HasPrefix(filespec, "!") {
//remove any existing files to del that the override covers
// and patterns later on that undo this take precedence
// first, remove ! ... note, replace ! with */ did not have
// expected effect with filepath.Match
filespec = strings.Replace(filespec, "!", "", 1)
// iterate through and determine ones to leave in
dontDel := []string{}
for candidate := range filesToDel {
compare := filepath.Join(workingDir, filespec)
glog.V(5).Infof("For %s and %s see if it matches the spec %s which means that we leave in\n", filespec, candidate, compare)
leaveIn, _ := filepath.Match(compare, candidate)
if leaveIn {
glog.V(5).Infof("Not removing %s \n", candidate)
dontDel = append(dontDel, candidate)
} else {
glog.V(5).Infof("No match for %s and %s \n", filespec, candidate)
}
}
// now remove any matches from files to delete list
for _, leaveIn := range dontDel {
delete(filesToDel, leaveIn)
}
continue
}
globspec := filepath.Join(workingDir, filespec)
glog.V(4).Infof("using globspec %s \n", globspec)
list, gerr := filepath.Glob(globspec)
if gerr != nil {
glog.V(4).Infof("Glob failed with %v \n", gerr)
} else {
for _, globresult := range list {
glog.V(5).Infof("Glob result %s \n", globresult)
filesToDel[globresult] = globresult
}
}
}
if err := scanner.Err(); err != nil && err != io.EOF {
glog.Errorf("Problem processing .s2iignore %v \n", err)
return nil, err
}
return filesToDel, nil
}
package file
import (
"fmt"
"path/filepath"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/constants"
"github.com/openshift/source-to-image/pkg/ignore"
"github.com/openshift/source-to-image/pkg/scm/git"
"github.com/openshift/source-to-image/pkg/util/fs"
utilglog "github.com/openshift/source-to-image/pkg/util/glog"
)
var glog = utilglog.StderrLog
// RecursiveCopyError indicates a copy operation failed because the destination is within the copy's source tree.
type RecursiveCopyError struct {
error
}
// File represents a simplest possible Downloader implementation where the
// sources are just copied from local directory.
type File struct {
fs.FileSystem
}
// Download copies sources from a local directory into the working directory.
// Caller guarantees that config.Source.IsLocal() is true.
func (f *File) Download(config *api.Config) (*git.SourceInfo, error) {
config.WorkingSourceDir = filepath.Join(config.WorkingDir, constants.Source)
copySrc := config.Source.LocalPath()
if len(config.ContextDir) > 0 {
copySrc = filepath.Join(copySrc, config.ContextDir)
}
glog.V(1).Infof("Copying sources from %q to %q", copySrc, config.WorkingSourceDir)
absWorkingSourceDir, err := filepath.Abs(config.WorkingSourceDir)
if err != nil {
return nil, err
}
absCopySrc, err := filepath.Abs(copySrc)
if err != nil {
return nil, err
}
if filepath.HasPrefix(absWorkingSourceDir, absCopySrc) {
return nil, RecursiveCopyError{error: fmt.Errorf("recursive copy requested, source directory %q contains the target directory %q", copySrc, config.WorkingSourceDir)}
}
di := ignore.DockerIgnorer{}
filesToIgnore, lerr := di.GetListOfFilesToIgnore(copySrc)
if lerr != nil {
return nil, lerr
}
if copySrc != config.WorkingSourceDir {
f.KeepSymlinks(config.KeepSymlinks)
err := f.CopyContents(copySrc, config.WorkingSourceDir, filesToIgnore)
if err != nil {
return nil, err
}
}
return &git.SourceInfo{
Location: config.Source.LocalPath(),
ContextDir: config.ContextDir,
}, nil
}
package git
import (
"os"
"path/filepath"
"runtime"
"github.com/golang/glog"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/constants"
"github.com/openshift/source-to-image/pkg/scm/git"
"github.com/openshift/source-to-image/pkg/util/fs"
)
// Clone knows how to clone a Git repository.
type Clone struct {
git.Git
fs.FileSystem
}
// Download downloads the application source code from the Git repository
// and checkout the Ref specified in the config.
func (c *Clone) Download(config *api.Config) (*git.SourceInfo, error) {
targetSourceDir := filepath.Join(config.WorkingDir, constants.Source)
config.WorkingSourceDir = targetSourceDir
ref := config.Source.URL.Fragment
if ref == "" {
ref = "HEAD"
}
if len(config.ContextDir) > 0 {
targetSourceDir = filepath.Join(config.WorkingDir, constants.ContextTmp)
glog.V(1).Infof("Downloading %q (%q) ...", config.Source, config.ContextDir)
} else {
glog.V(1).Infof("Downloading %q ...", config.Source)
}
if !config.IgnoreSubmodules {
glog.V(2).Infof("Cloning sources into %q", targetSourceDir)
} else {
glog.V(2).Infof("Cloning sources (ignoring submodules) into %q", targetSourceDir)
}
cloneConfig := git.CloneConfig{Quiet: true}
err := c.Clone(config.Source, targetSourceDir, cloneConfig)
if err != nil {
glog.V(0).Infof("error: git clone failed: %v", err)
return nil, err
}
err = c.Checkout(targetSourceDir, ref)
if err != nil {
return nil, err
}
glog.V(1).Infof("Checked out %q", ref)
if !config.IgnoreSubmodules {
err = c.SubmoduleUpdate(targetSourceDir, true, true)
if err != nil {
return nil, err
}
glog.V(1).Infof("Updated submodules for %q", ref)
}
// Record Git's knowledge about file permissions
if runtime.GOOS == "windows" {
filemodes, err := c.LsTree(filepath.Join(targetSourceDir, config.ContextDir), ref, true)
if err != nil {
return nil, err
}
for _, filemode := range filemodes {
c.Chmod(filepath.Join(targetSourceDir, config.ContextDir, filemode.Name()), os.FileMode(filemode.Mode())&os.ModePerm)
}
}
info := c.GetInfo(targetSourceDir)
if len(config.ContextDir) > 0 {
originalTargetDir := filepath.Join(config.WorkingDir, constants.Source)
c.RemoveDirectory(originalTargetDir)
path := filepath.Join(targetSourceDir, config.ContextDir)
err := c.CopyContents(path, originalTargetDir, nil)
if err != nil {
return nil, err
}
c.RemoveDirectory(targetSourceDir)
}
if len(config.ContextDir) > 0 {
info.ContextDir = config.ContextDir
}
return info, nil
}
package git
import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
log "github.com/golang/glog"
"github.com/openshift/source-to-image/pkg/util/cmd"
"github.com/openshift/source-to-image/pkg/util/cygpath"
"github.com/openshift/source-to-image/pkg/util/fs"
utilglog "github.com/openshift/source-to-image/pkg/util/glog"
)
var glog = utilglog.StderrLog
var lsTreeRegexp = regexp.MustCompile("([0-7]{6}) [^ ]+ [0-9a-f]{40}\t(.*)")
// Git is an interface used by main STI code to extract/checkout git repositories
type Git interface {
Clone(source *URL, target string, opts CloneConfig) error
Checkout(repo, ref string) error
SubmoduleUpdate(repo string, init, recursive bool) error
LsTree(repo, ref string, recursive bool) ([]os.FileInfo, error)
GetInfo(string) *SourceInfo
}
// New returns a new instance of the default implementation of the Git interface
func New(fs fs.FileSystem, runner cmd.CommandRunner) Git {
return &stiGit{
FileSystem: fs,
CommandRunner: runner,
}
}
type stiGit struct {
fs.FileSystem
cmd.CommandRunner
}
func cloneConfigToArgs(opts CloneConfig) []string {
result := []string{}
if opts.Quiet {
result = append(result, "--quiet")
}
if opts.Recursive {
result = append(result, "--recursive")
}
return result
}
// followGitSubmodule looks at a .git /file/ and tries to retrieve from inside
// it the gitdir value, which is supposed to indicate the location of the
// corresponding .git /directory/. Note: the gitdir value should point directly
// to the corresponding .git directory even in the case of nested submodules.
func followGitSubmodule(fs fs.FileSystem, gitPath string) (string, error) {
f, err := os.Open(gitPath)
if err != nil {
return "", err
}
defer f.Close()
sc := bufio.NewScanner(f)
if sc.Scan() {
s := sc.Text()
if strings.HasPrefix(s, "gitdir: ") {
newGitPath := s[8:]
if !filepath.IsAbs(newGitPath) {
newGitPath = filepath.Join(filepath.Dir(gitPath), newGitPath)
}
fi, err := fs.Stat(newGitPath)
if err != nil && !os.IsNotExist(err) {
return "", err
}
if os.IsNotExist(err) || !fi.IsDir() {
return "", fmt.Errorf("gitdir link in .git file %q is invalid", gitPath)
}
return newGitPath, nil
}
}
return "", fmt.Errorf("unable to parse .git file %q", gitPath)
}
// IsLocalNonBareGitRepository returns true if dir hosts a non-bare git
// repository, i.e. it contains a ".git" subdirectory or file (submodule case).
func IsLocalNonBareGitRepository(fs fs.FileSystem, dir string) (bool, error) {
_, err := fs.Stat(filepath.Join(dir, ".git"))
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
// LocalNonBareGitRepositoryIsEmpty returns true if the non-bare git repository
// at dir has no refs or objects. It also handles the case of dir being a
// checked out git submodule.
func LocalNonBareGitRepositoryIsEmpty(fs fs.FileSystem, dir string) (bool, error) {
gitPath := filepath.Join(dir, ".git")
fi, err := fs.Stat(gitPath)
if err != nil {
return false, err
}
if !fi.IsDir() {
gitPath, err = followGitSubmodule(fs, gitPath)
if err != nil {
return false, err
}
}
// Search for any file in .git/{objects,refs}. We don't just search the
// base .git directory because of the hook samples that are normally
// generated with `git init`
found := false
for _, dir := range []string{"objects", "refs"} {
err := fs.Walk(filepath.Join(gitPath, dir), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
found = true
}
if found {
return filepath.SkipDir
}
return nil
})
if err != nil {
return false, err
}
if found {
return false, nil
}
}
return true, nil
}
// HasGitBinary checks if the 'git' binary is available on the system
func HasGitBinary() bool {
_, err := exec.LookPath("git")
return err == nil
}
// Clone clones a git repository to a specific target directory.
func (h *stiGit) Clone(src *URL, target string, c CloneConfig) error {
var err error
source := *src
if cygpath.UsingCygwinGit {
if source.IsLocal() {
source.URL.Path, err = cygpath.ToSlashCygwin(source.LocalPath())
if err != nil {
return err
}
}
target, err = cygpath.ToSlashCygwin(target)
if err != nil {
return err
}
}
cloneArgs := append([]string{"clone"}, cloneConfigToArgs(c)...)
cloneArgs = append(cloneArgs, []string{source.StringNoFragment(), target}...)
stderr := &bytes.Buffer{}
opts := cmd.CommandOpts{Stderr: stderr}
err = h.RunWithOptions(opts, "git", cloneArgs...)
if err != nil {
glog.Errorf("Clone failed: source %s, target %s, with output %q", source, target, stderr.String())
return err
}
return nil
}
// Checkout checks out a specific branch reference of a given git repository
func (h *stiGit) Checkout(repo, ref string) error {
opts := cmd.CommandOpts{
Stdout: os.Stdout,
Stderr: os.Stderr,
Dir: repo,
}
if log.V(1) {
return h.RunWithOptions(opts, "git", "checkout", ref)
}
return h.RunWithOptions(opts, "git", "checkout", "--quiet", ref)
}
// SubmoduleInit initializes/clones submodules
func (h *stiGit) SubmoduleInit(repo string) error {
opts := cmd.CommandOpts{
Stdout: os.Stdout,
Stderr: os.Stderr,
Dir: repo,
}
return h.RunWithOptions(opts, "git", "submodule", "init")
}
// SubmoduleUpdate checks out submodules to their correct version.
// Optionally also inits submodules, optionally operates recursively.
func (h *stiGit) SubmoduleUpdate(repo string, init, recursive bool) error {
updateArgs := []string{"submodule", "update"}
if init {
updateArgs = append(updateArgs, "--init")
}
if recursive {
updateArgs = append(updateArgs, "--recursive")
}
opts := cmd.CommandOpts{
Stdout: os.Stdout,
Stderr: os.Stderr,
Dir: repo,
}
return h.RunWithOptions(opts, "git", updateArgs...)
}
// LsTree returns a slice of os.FileInfo objects populated with the paths and
// file modes of files known to Git. This is used on Windows systems where the
// executable mode metadata is lost on git checkout.
func (h *stiGit) LsTree(repo, ref string, recursive bool) ([]os.FileInfo, error) {
args := []string{"ls-tree", ref}
if recursive {
args = append(args, "-r")
}
opts := cmd.CommandOpts{
Dir: repo,
}
r, err := h.StartWithStdoutPipe(opts, "git", args...)
if err != nil {
return nil, err
}
submodules := []string{}
rv := []os.FileInfo{}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
text := scanner.Text()
m := lsTreeRegexp.FindStringSubmatch(text)
if m == nil {
return nil, fmt.Errorf("unparsable response %q from git ls-files", text)
}
mode, _ := strconv.ParseInt(m[1], 8, 0)
path := m[2]
if recursive && mode == 0160000 { // S_IFGITLINK
submodules = append(submodules, filepath.Join(repo, path))
continue
}
rv = append(rv, &fs.FileInfo{FileMode: os.FileMode(mode), FileName: path})
}
err = scanner.Err()
if err != nil {
h.Wait()
return nil, err
}
err = h.Wait()
if err != nil {
return nil, err
}
for _, submodule := range submodules {
rrv, err := h.LsTree(submodule, "HEAD", recursive)
if err != nil {
return nil, err
}
rv = append(rv, rrv...)
}
return rv, nil
}
// GetInfo retrieves the information about the source code and commit
func (h *stiGit) GetInfo(repo string) *SourceInfo {
git := func(arg ...string) string {
command := exec.Command("git", arg...)
command.Dir = repo
out, err := command.CombinedOutput()
if err != nil {
glog.V(1).Infof("Error executing 'git %#v': %s (%v)", arg, out, err)
return ""
}
return strings.TrimSpace(string(out))
}
return &SourceInfo{
Location: git("config", "--get", "remote.origin.url"),
Ref: git("rev-parse", "--abbrev-ref", "HEAD"),
CommitID: git("rev-parse", "--verify", "HEAD"),
AuthorName: git("--no-pager", "show", "-s", "--format=%an", "HEAD"),
AuthorEmail: git("--no-pager", "show", "-s", "--format=%ae", "HEAD"),
CommitterName: git("--no-pager", "show", "-s", "--format=%cn", "HEAD"),
CommitterEmail: git("--no-pager", "show", "-s", "--format=%ce", "HEAD"),
Date: git("--no-pager", "show", "-s", "--format=%ad", "HEAD"),
Message: git("--no-pager", "show", "-s", "--format=%<(80,trunc)%s", "HEAD"),
}
}
package git
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/openshift/source-to-image/pkg/util/cmd"
"github.com/openshift/source-to-image/pkg/util/cygpath"
)
// CreateLocalGitDirectory creates a git directory with a commit
func CreateLocalGitDirectory() (string, error) {
cr := cmd.NewCommandRunner()
dir, err := CreateEmptyLocalGitDirectory()
if err != nil {
return "", err
}
f, err := os.Create(filepath.Join(dir, "testfile"))
if err != nil {
return "", err
}
f.Close()
err = cr.RunWithOptions(cmd.CommandOpts{Dir: dir}, "git", "add", ".")
if err != nil {
return "", err
}
err = cr.RunWithOptions(cmd.CommandOpts{Dir: dir, EnvAppend: []string{"GIT_AUTHOR_NAME=test", "GIT_AUTHOR_EMAIL=test@test", "GIT_COMMITTER_NAME=test", "GIT_COMMITTER_EMAIL=test@test"}}, "git", "commit", "-m", "testcommit")
if err != nil {
return "", err
}
return dir, nil
}
// CreateEmptyLocalGitDirectory creates a git directory with no checkin yet
func CreateEmptyLocalGitDirectory() (string, error) {
cr := cmd.NewCommandRunner()
dir, err := ioutil.TempDir(os.TempDir(), "gitdir-s2i-test")
if err != nil {
return "", err
}
err = cr.RunWithOptions(cmd.CommandOpts{Dir: dir}, "git", "init")
if err != nil {
return "", err
}
return dir, nil
}
// CreateLocalGitDirectoryWithSubmodule creates a git directory with a submodule
func CreateLocalGitDirectoryWithSubmodule() (string, error) {
cr := cmd.NewCommandRunner()
submodule, err := CreateLocalGitDirectory()
if err != nil {
return "", err
}
defer os.RemoveAll(submodule)
if cygpath.UsingCygwinGit {
var err error
submodule, err = cygpath.ToSlashCygwin(submodule)
if err != nil {
return "", err
}
}
dir, err := CreateEmptyLocalGitDirectory()
if err != nil {
return "", err
}
err = cr.RunWithOptions(cmd.CommandOpts{Dir: dir}, "git", "submodule", "add", submodule, "submodule")
if err != nil {
return "", err
}
return dir, nil
}
package git
import (
"fmt"
"net/url"
"path/filepath"
"regexp"
"runtime"
"strings"
)
// According to git-clone(1), a "Git URL" can be in one of three broad types:
// 1) A standards-compliant URL.
// a) The scheme may be followed by '://',
// e.g. https://github.com/openshift/origin, file:///foo/bar, etc. In
// this case, note that among other things, a standards-compliant file URL
// must have an empty host part, an absolute path and no backslashes. The
// Git for Windows URL parser accepts many non-compliant URLs, but we
// don't.
// b) Alternatively, the scheme may be followed by '::', in which case it is
// treated as an transport/opaque address pair, e.g.
// http::http://github.com/openshift/origin.git .
// 2) The "alternative scp-like syntax", including a ':' with no preceding '/',
// but not of the form C:... on Windows, e.g.
// git@github.com:openshift/origin, etc.
// 3) An OS-specific relative or absolute local file path, e.g. foo/bar,
// C:\foo\bar, etc.
//
// We extend all of the above URL types to additionally accept an optional
// appended #fragment, which is given to specify a git reference.
//
// The git client allows Git URL rewriting rules to be defined. The meaning of
// a Git URL cannot be 100% guaranteed without consulting the rewriting rules.
// URLType indicates the type of the URL (see above)
type URLType int
const (
// URLTypeURL is the URL type (see above)
URLTypeURL URLType = iota
// URLTypeSCP is the SCP type (see above)
URLTypeSCP
// URLTypeLocal is the local type (see above)
URLTypeLocal
)
// String returns a string representation of the URLType
func (t URLType) String() string {
switch t {
case URLTypeURL:
return "URLTypeURL"
case URLTypeSCP:
return "URLTypeSCP"
case URLTypeLocal:
return "URLTypeLocal"
}
panic("unknown URLType")
}
// GoString returns a Go string representation of the URLType
func (t URLType) GoString() string {
return t.String()
}
// URL represents a "Git URL"
type URL struct {
URL url.URL
Type URLType
}
var urlSchemeRegexp = regexp.MustCompile("(?i)^[a-z][-a-z0-9+.]*:") // matches scheme: according to RFC3986
var dosDriveRegexp = regexp.MustCompile("(?i)^[a-z]:")
var scpRegexp = regexp.MustCompile("^" +
"(?:([^@/]*)@)?" + // user@ (optional)
"([^/]*):" + // host:
"(.*)" + // path
"$")
func splitOnByte(s string, c byte) (string, string) {
if i := strings.IndexByte(s, c); i != -1 {
return s[:i], s[i+1:]
}
return s, ""
}
// Parse parses a "Git URL"
func Parse(rawurl string) (*URL, error) {
if urlSchemeRegexp.MatchString(rawurl) &&
(runtime.GOOS != "windows" || !dosDriveRegexp.MatchString(rawurl)) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
if u.Scheme == "file" && u.Opaque == "" {
if u.Host != "" {
return nil, fmt.Errorf("file url %q has non-empty host %q", rawurl, u.Host)
}
if runtime.GOOS == "windows" && (len(u.Path) == 0 || !filepath.IsAbs(u.Path[1:])) {
return nil, fmt.Errorf("file url %q has non-absolute path %q", rawurl, u.Path)
}
}
return &URL{
URL: *u,
Type: URLTypeURL,
}, nil
}
s, fragment := splitOnByte(rawurl, '#')
if m := scpRegexp.FindStringSubmatch(s); m != nil &&
(runtime.GOOS != "windows" || !dosDriveRegexp.MatchString(s)) {
u := &url.URL{
Host: m[2],
Path: m[3],
Fragment: fragment,
}
if m[1] != "" {
u.User = url.User(m[1])
}
return &URL{
URL: *u,
Type: URLTypeSCP,
}, nil
}
return &URL{
URL: url.URL{
Path: s,
Fragment: fragment,
},
Type: URLTypeLocal,
}, nil
}
// MustParse parses a "Git URL" and panics on failure
func MustParse(rawurl string) *URL {
u, err := Parse(rawurl)
if err != nil {
panic(err)
}
return u
}
// String returns a string representation of the URL
func (u URL) String() string {
var s string
switch u.Type {
case URLTypeURL:
return u.URL.String()
case URLTypeSCP:
if u.URL.User != nil {
s = u.URL.User.Username() + "@"
}
s += u.URL.Host + ":" + u.URL.Path
case URLTypeLocal:
s = u.URL.Path
}
if u.URL.RawQuery != "" {
s += "?" + u.URL.RawQuery
}
if u.URL.Fragment != "" {
s += "#" + u.URL.Fragment
}
return s
}
// StringNoFragment returns a string representation of the URL without its
// fragment
func (u URL) StringNoFragment() string {
u.URL.Fragment = ""
return u.String()
}
// IsLocal returns true if the Git URL refers to a local repository
func (u URL) IsLocal() bool {
return u.Type == URLTypeLocal || (u.Type == URLTypeURL && u.URL.Scheme == "file" && u.URL.Opaque == "")
}
// LocalPath returns the path to a local repository in OS-native format. It is
// assumed that IsLocal() is true
func (u URL) LocalPath() string {
switch {
case u.Type == URLTypeLocal:
return u.URL.Path
case u.Type == URLTypeURL && u.URL.Scheme == "file" && u.URL.Opaque == "":
if runtime.GOOS == "windows" && len(u.URL.Path) > 0 && u.URL.Path[0] == '/' {
return filepath.FromSlash(u.URL.Path[1:])
}
return filepath.FromSlash(u.URL.Path)
}
panic("LocalPath called on non-local URL")
}
package scm
import (
"github.com/openshift/source-to-image/pkg/build"
"github.com/openshift/source-to-image/pkg/errors"
"github.com/openshift/source-to-image/pkg/scm/downloaders/empty"
"github.com/openshift/source-to-image/pkg/scm/downloaders/file"
gitdownloader "github.com/openshift/source-to-image/pkg/scm/downloaders/git"
"github.com/openshift/source-to-image/pkg/scm/git"
"github.com/openshift/source-to-image/pkg/util/cmd"
"github.com/openshift/source-to-image/pkg/util/fs"
utilglog "github.com/openshift/source-to-image/pkg/util/glog"
)
var glog = utilglog.StderrLog
// DownloaderForSource determines what SCM plugin should be used for downloading
// the sources from the repository.
func DownloaderForSource(fs fs.FileSystem, s *git.URL, forceCopy bool) (build.Downloader, error) {
glog.V(4).Infof("DownloadForSource %s", s)
if s == nil {
return &empty.Noop{}, nil
}
if s.IsLocal() {
if forceCopy {
return &file.File{FileSystem: fs}, nil
}
isLocalNonBareGitRepo, err := git.IsLocalNonBareGitRepository(fs, s.LocalPath())
if err != nil {
return nil, err
}
if !isLocalNonBareGitRepo {
return &file.File{FileSystem: fs}, nil
}
isEmpty, err := git.LocalNonBareGitRepositoryIsEmpty(fs, s.LocalPath())
if err != nil {
return nil, err
}
if isEmpty {
return nil, errors.NewEmptyGitRepositoryError(s.LocalPath())
}
if !git.HasGitBinary() {
return &file.File{FileSystem: fs}, nil
}
}
return &gitdownloader.Clone{Git: git.New(fs, cmd.NewCommandRunner()), FileSystem: fs}, nil
}
package scripts
import (
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"sync"
"github.com/openshift/source-to-image/pkg/api"
s2ierr "github.com/openshift/source-to-image/pkg/errors"
"github.com/openshift/source-to-image/pkg/scm/git"
utilglog "github.com/openshift/source-to-image/pkg/util/glog"
)
var glog = utilglog.StderrLog
// Downloader downloads the specified URL to the target file location
type Downloader interface {
Download(url *url.URL, target string) (*git.SourceInfo, error)
}
// schemeReader creates an io.Reader from the given url.
type schemeReader interface {
Read(*url.URL) (io.ReadCloser, error)
}
type downloader struct {
schemeReaders map[string]schemeReader
}
// NewDownloader creates an instance of the default Downloader implementation
func NewDownloader(proxyConfig *api.ProxyConfig) Downloader {
httpReader := NewHTTPURLReader(proxyConfig)
return &downloader{
schemeReaders: map[string]schemeReader{
"http": httpReader,
"https": httpReader,
"file": &FileURLReader{},
"image": &ImageReader{},
},
}
}
// Download downloads the file pointed to by URL into local targetFile
// Returns information a boolean flag informing whether any download/copy operation
// happened and an error if there was a problem during that operation
func (d *downloader) Download(url *url.URL, targetFile string) (*git.SourceInfo, error) {
r := d.schemeReaders[url.Scheme]
info := &git.SourceInfo{}
if r == nil {
glog.Errorf("No URL handler found for %s", url.String())
return nil, s2ierr.NewURLHandlerError(url.String())
}
reader, err := r.Read(url)
if err != nil {
return nil, err
}
defer reader.Close()
out, err := os.Create(targetFile)
defer out.Close()
if err != nil {
glog.Errorf("Unable to create target file %s (%v)", targetFile, err)
return nil, err
}
if _, err = io.Copy(out, reader); err != nil {
os.Remove(targetFile)
glog.Warningf("Skipping file %s due to error copying from source: %v", targetFile, err)
return nil, err
}
glog.V(2).Infof("Downloaded '%s'", url.String())
info.Location = url.String()
return info, nil
}
// HTTPURLReader retrieves a response from a given HTTP(S) URL.
type HTTPURLReader struct {
Get func(url string) (*http.Response, error)
}
var transportMap map[api.ProxyConfig]*http.Transport
var transportMapMutex sync.Mutex
func init() {
transportMap = make(map[api.ProxyConfig]*http.Transport)
}
// NewHTTPURLReader returns a new HTTPURLReader.
func NewHTTPURLReader(proxyConfig *api.ProxyConfig) *HTTPURLReader {
getFunc := http.Get
if proxyConfig != nil {
transportMapMutex.Lock()
transport, ok := transportMap[*proxyConfig]
if !ok {
transport = &http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
if proxyConfig.HTTPSProxy != nil && req.URL.Scheme == "https" {
return proxyConfig.HTTPSProxy, nil
}
return proxyConfig.HTTPProxy, nil
},
}
transportMap[*proxyConfig] = transport
}
transportMapMutex.Unlock()
client := &http.Client{
Transport: transport,
}
getFunc = client.Get
}
return &HTTPURLReader{Get: getFunc}
}
// Read produces an io.Reader from an http(s) URL.
func (h *HTTPURLReader) Read(url *url.URL) (io.ReadCloser, error) {
resp, err := h.Get(url.String())
if err != nil {
if resp != nil {
defer resp.Body.Close()
}
return nil, err
}
if resp.StatusCode == 200 || resp.StatusCode == 201 {
return resp.Body, nil
}
return nil, s2ierr.NewDownloadError(url.String(), resp.StatusCode)
}
// FileURLReader opens a specified file and returns its stream
type FileURLReader struct{}
// Read produces an io.Reader from a file URL
func (*FileURLReader) Read(url *url.URL) (io.ReadCloser, error) {
// for some reason url.Host may contain information about the ./ or ../ when
// specifying relative path, thus using that value as well
return os.Open(filepath.Join(url.Host, url.Path))
}
// ImageReader just returns information the URL is from inside the image
type ImageReader struct{}
// Read throws Not implemented error
func (*ImageReader) Read(url *url.URL) (io.ReadCloser, error) {
return nil, s2ierr.NewScriptsInsideImageError(url.String())
}
package scripts
import (
"bufio"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/constants"
)
// GetEnvironment gets the .s2i/environment file located in the sources and
// parse it into EnvironmentList.
func GetEnvironment(path string) (api.EnvironmentList, error) {
envPath := filepath.Join(path, ".s2i", constants.Environment)
if _, err := os.Stat(envPath); os.IsNotExist(err) {
return nil, errors.New("no environment file found in application sources")
}
f, err := os.Open(envPath)
if err != nil {
return nil, errors.New("unable to read custom environment file")
}
defer f.Close()
result := api.EnvironmentList{}
scanner := bufio.NewScanner(f)
for scanner.Scan() {
s := scanner.Text()
// Allow for comments in environment file
if strings.HasPrefix(s, "#") {
continue
}
result.Set(s)
}
glog.V(1).Infof("Setting %d environment variables provided by environment file in sources", len(result))
return result, scanner.Err()
}
// ConvertEnvironmentList converts the EnvironmentList to "key=val" strings.
func ConvertEnvironmentList(env api.EnvironmentList) (result []string) {
for _, e := range env {
result = append(result, fmt.Sprintf("%s=%s", e.Name, e.Value))
}
return
}
// ConvertEnvironmentToDocker converts the EnvironmentList into Dockerfile format.
func ConvertEnvironmentToDocker(env api.EnvironmentList) (result string) {
for i, e := range env {
if i == 0 {
result += fmt.Sprintf("ENV %s=\"%s\"", e.Name, escape(e.Value))
} else {
result += fmt.Sprintf(" \\\n %s=\"%s\"", e.Name, escape(e.Value))
}
}
result += "\n"
return
}
// escape returns the passed-in value, escaped so that it will not undergo
// expansion in an ENV instruction.
func escape(value string) string {
result := ""
for _, ch := range value {
if strings.ContainsRune(`$"\`, ch) {
result += `\`
}
result += string(ch)
}
return result
}
package scripts
import (
"fmt"
"net/url"
"path/filepath"
"strings"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/constants"
"github.com/openshift/source-to-image/pkg/docker"
s2ierr "github.com/openshift/source-to-image/pkg/errors"
"github.com/openshift/source-to-image/pkg/util/fs"
)
// Installer interface is responsible for installing scripts needed to run the
// build.
type Installer interface {
InstallRequired(scripts []string, dstDir string) ([]api.InstallResult, error)
InstallOptional(scripts []string, dstDir string) []api.InstallResult
}
// ScriptHandler provides an interface for various scripts source handlers.
type ScriptHandler interface {
Get(script string) *api.InstallResult
Install(*api.InstallResult) error
SetDestinationDir(string)
String() string
}
// URLScriptHandler handles script download using URL.
type URLScriptHandler struct {
URL string
DestinationDir string
Download Downloader
FS fs.FileSystem
Name string
}
const (
sourcesRootAbbrev = "<source-dir>"
// ScriptURLHandler is the name of the script URL handler
ScriptURLHandler = "script URL handler"
// ImageURLHandler is the name of the image URL handler
ImageURLHandler = "image URL handler"
// SourceHandler is the name of the source script handler
SourceHandler = "source handler"
)
var (
// RequiredScripts must be present to do an s2i build
RequiredScripts = []string{constants.Assemble, constants.Run}
// OptionalScripts may be provided when doing an s2i build
OptionalScripts = []string{constants.SaveArtifacts}
)
// SetDestinationDir sets the destination where the scripts should be
// downloaded.
func (s *URLScriptHandler) SetDestinationDir(baseDir string) {
s.DestinationDir = baseDir
}
// String implements the String() function.
func (s *URLScriptHandler) String() string {
return s.Name
}
// Get parses the provided URL and the script name.
func (s *URLScriptHandler) Get(script string) *api.InstallResult {
if len(s.URL) == 0 {
return nil
}
scriptURL, err := url.ParseRequestURI(s.URL + "/" + script)
if err != nil {
glog.Infof("invalid script url %q: %v", s.URL, err)
return nil
}
return &api.InstallResult{
Script: script,
URL: scriptURL.String(),
}
}
// Install downloads the script and fix its permissions.
func (s *URLScriptHandler) Install(r *api.InstallResult) error {
downloadURL, err := url.Parse(r.URL)
if err != nil {
return err
}
dst := filepath.Join(s.DestinationDir, constants.UploadScripts, r.Script)
if _, err := s.Download.Download(downloadURL, dst); err != nil {
if e, ok := err.(s2ierr.Error); ok {
if e.ErrorCode == s2ierr.ScriptsInsideImageError {
r.Installed = true
return nil
}
}
return err
}
if err := s.FS.Chmod(dst, 0755); err != nil {
return err
}
r.Installed = true
r.Downloaded = true
return nil
}
// SourceScriptHandler handles the case when the scripts are contained in the
// source code directory.
type SourceScriptHandler struct {
DestinationDir string
fs fs.FileSystem
}
// Get verifies if the script is present in the source directory and get the
// installation result.
func (s *SourceScriptHandler) Get(script string) *api.InstallResult {
location := filepath.Join(s.DestinationDir, constants.SourceScripts, script)
if s.fs.Exists(location) {
return &api.InstallResult{Script: script, URL: location}
}
// TODO: The '.sti/bin' path inside the source code directory is deprecated
// and this should (and will) be removed soon.
location = filepath.FromSlash(strings.Replace(filepath.ToSlash(location), "s2i/bin", "sti/bin", 1))
if s.fs.Exists(location) {
glog.Info("DEPRECATED: Use .s2i/bin instead of .sti/bin")
return &api.InstallResult{Script: script, URL: location}
}
return nil
}
// String implements the String() function.
func (s *SourceScriptHandler) String() string {
return SourceHandler
}
// Install copies the script into upload directory and fix its permissions.
func (s *SourceScriptHandler) Install(r *api.InstallResult) error {
dst := filepath.Join(s.DestinationDir, constants.UploadScripts, r.Script)
if err := s.fs.Rename(r.URL, dst); err != nil {
return err
}
if err := s.fs.Chmod(dst, 0755); err != nil {
return err
}
// Make the path to scripts nicer in logs
parts := strings.Split(filepath.ToSlash(r.URL), "/")
if len(parts) > 3 {
r.URL = filepath.FromSlash(sourcesRootAbbrev + "/" + strings.Join(parts[len(parts)-3:], "/"))
}
r.Installed = true
r.Downloaded = true
return nil
}
// SetDestinationDir sets the directory where the scripts should be uploaded.
// In case of SourceScriptHandler this is a source directory root.
func (s *SourceScriptHandler) SetDestinationDir(baseDir string) {
s.DestinationDir = baseDir
}
// ScriptSourceManager manages various script handlers.
type ScriptSourceManager interface {
Add(ScriptHandler)
SetDownloader(Downloader)
Installer
}
// DefaultScriptSourceManager manages the default script lookup and installation
// for source-to-image.
type DefaultScriptSourceManager struct {
Image string
ScriptsURL string
download Downloader
docker docker.Docker
dockerAuth api.AuthConfig
sources []ScriptHandler
fs fs.FileSystem
}
// Add registers a new script source handler.
func (m *DefaultScriptSourceManager) Add(s ScriptHandler) {
if len(m.sources) == 0 {
m.sources = []ScriptHandler{}
}
m.sources = append(m.sources, s)
}
// NewInstaller returns a new instance of the default Installer implementation
func NewInstaller(image string, scriptsURL string, proxyConfig *api.ProxyConfig, docker docker.Docker, auth api.AuthConfig, fs fs.FileSystem) Installer {
m := DefaultScriptSourceManager{
Image: image,
ScriptsURL: scriptsURL,
dockerAuth: auth,
docker: docker,
fs: fs,
download: NewDownloader(proxyConfig),
}
// Order is important here, first we try to get the scripts from provided URL,
// then we look into sources and check for .s2i/bin scripts.
if len(m.ScriptsURL) > 0 {
m.Add(&URLScriptHandler{URL: m.ScriptsURL, Download: m.download, FS: m.fs, Name: ScriptURLHandler})
}
m.Add(&SourceScriptHandler{fs: m.fs})
if m.docker != nil {
// If the detection handlers above fail, try to get the script url from the
// docker image itself.
defaultURL, err := m.docker.GetScriptsURL(m.Image)
if err == nil && defaultURL != "" {
m.Add(&URLScriptHandler{URL: defaultURL, Download: m.download, FS: m.fs, Name: ImageURLHandler})
}
}
return &m
}
// InstallRequired Downloads and installs required scripts into dstDir, the result is a
// map of scripts with detailed information about each of the scripts install process
// with error if installing some of them failed
func (m *DefaultScriptSourceManager) InstallRequired(scripts []string, dstDir string) ([]api.InstallResult, error) {
result := m.InstallOptional(scripts, dstDir)
failedScripts := []string{}
var err error
for _, r := range result {
if r.Error != nil {
failedScripts = append(failedScripts, r.Script)
}
}
if len(failedScripts) > 0 {
err = s2ierr.NewInstallRequiredError(failedScripts, constants.ScriptsURLLabel)
}
return result, err
}
// InstallOptional downloads and installs a set of scripts into dstDir, the result is a
// map of scripts with detailed information about each of the scripts install process
func (m *DefaultScriptSourceManager) InstallOptional(scripts []string, dstDir string) []api.InstallResult {
result := []api.InstallResult{}
for _, script := range scripts {
installed := false
failedSources := []string{}
for _, e := range m.sources {
detected := false
h := e.(ScriptHandler)
h.SetDestinationDir(dstDir)
if r := h.Get(script); r != nil {
if err := h.Install(r); err != nil {
failedSources = append(failedSources, h.String())
// all this means is this source didn't have this particular script
glog.V(4).Infof("script %q found by the %s, but failed to install: %v", script, h, err)
} else {
r.FailedSources = failedSources
result = append(result, *r)
installed = true
detected = true
glog.V(4).Infof("Using %q installed from %q", script, r.URL)
}
}
if detected {
break
}
}
if !installed {
result = append(result, api.InstallResult{
FailedSources: failedSources,
Script: script,
Error: fmt.Errorf("script %q not installed", script),
})
}
}
return result
}
package tar
import (
"archive/tar"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/openshift/source-to-image/pkg/util/fs"
utilglog "github.com/openshift/source-to-image/pkg/util/glog"
s2ierr "github.com/openshift/source-to-image/pkg/errors"
"github.com/openshift/source-to-image/pkg/util"
)
var glog = utilglog.StderrLog
// defaultTimeout is the amount of time that the untar will wait for a tar
// stream to extract a single file. A timeout is needed to guard against broken
// connections in which it would wait for a long time to untar and nothing would happen
const defaultTimeout = 300 * time.Second
// DefaultExclusionPattern is the pattern of files that will not be included in a tar
// file when creating one. By default it is any file inside a .git metadata directory
var DefaultExclusionPattern = regexp.MustCompile(`(^|/)\.git(/|$)`)
// Tar can create and extract tar files used in an STI build
type Tar interface {
// SetExclusionPattern sets the exclusion pattern for tar
// creation
SetExclusionPattern(*regexp.Regexp)
// CreateTarFile creates a tar file in the base directory
// using the contents of dir directory
// The name of the new tar file is returned if successful
CreateTarFile(base, dir string) (string, error)
// CreateTarStreamToTarWriter creates a tar from the given directory
// and streams it to the given writer.
// An error is returned if an error occurs during streaming.
// Archived file names are written to the logger if provided
CreateTarStreamToTarWriter(dir string, includeDirInPath bool, writer Writer, logger io.Writer) error
// CreateTarStream creates a tar from the given directory
// and streams it to the given writer.
// An error is returned if an error occurs during streaming.
CreateTarStream(dir string, includeDirInPath bool, writer io.Writer) error
// CreateTarStreamReader returns an io.ReadCloser from which a tar stream can be
// read. The tar stream is created using CreateTarStream.
CreateTarStreamReader(dir string, includeDirInPath bool) io.ReadCloser
// ExtractTarStream extracts files from a given tar stream.
// Times out if reading from the stream for any given file
// exceeds the value of timeout.
ExtractTarStream(dir string, reader io.Reader) error
// ExtractTarStreamWithLogging extracts files from a given tar stream.
// Times out if reading from the stream for any given file
// exceeds the value of timeout.
// Extracted file names are written to the logger if provided.
ExtractTarStreamWithLogging(dir string, reader io.Reader, logger io.Writer) error
// ExtractTarStreamFromTarReader extracts files from a given tar stream.
// Times out if reading from the stream for any given file
// exceeds the value of timeout.
// Extracted file names are written to the logger if provided.
ExtractTarStreamFromTarReader(dir string, tarReader Reader, logger io.Writer) error
}
// Reader is an interface which tar.Reader implements.
type Reader interface {
io.Reader
Next() (*tar.Header, error)
}
// Writer is an interface which tar.Writer implements.
type Writer interface {
io.WriteCloser
Flush() error
WriteHeader(hdr *tar.Header) error
}
// ChmodAdapter changes the mode of files and directories inline as a tarfile is
// being written
type ChmodAdapter struct {
Writer
NewFileMode int64
NewExecFileMode int64
NewDirMode int64
}
// WriteHeader changes the mode of files and directories inline as a tarfile is
// being written
func (a ChmodAdapter) WriteHeader(hdr *tar.Header) error {
if hdr.FileInfo().Mode()&os.ModeSymlink == 0 {
newMode := hdr.Mode &^ 0777
if hdr.FileInfo().IsDir() {
newMode |= a.NewDirMode
} else if hdr.FileInfo().Mode()&0010 != 0 { // S_IXUSR
newMode |= a.NewExecFileMode
} else {
newMode |= a.NewFileMode
}
hdr.Mode = newMode
}
return a.Writer.WriteHeader(hdr)
}
// RenameAdapter renames files and directories inline as a tarfile is being
// written
type RenameAdapter struct {
Writer
Old string
New string
}
// WriteHeader renames files and directories inline as a tarfile is being
// written
func (a RenameAdapter) WriteHeader(hdr *tar.Header) error {
if hdr.Name == a.Old {
hdr.Name = a.New
} else if strings.HasPrefix(hdr.Name, a.Old+"/") {
hdr.Name = a.New + hdr.Name[len(a.Old):]
}
return a.Writer.WriteHeader(hdr)
}
// New creates a new Tar
func New(fs fs.FileSystem) Tar {
return &stiTar{
FileSystem: fs,
exclude: DefaultExclusionPattern,
timeout: defaultTimeout,
}
}
// NewWithTimeout creates a new Tar with the provided timeout extracting files.
func NewWithTimeout(fs fs.FileSystem, timeout time.Duration) Tar {
return &stiTar{
FileSystem: fs,
exclude: DefaultExclusionPattern,
timeout: timeout,
}
}
// NewParanoid creates a new Tar that has restrictions
// on what it can do while extracting files.
func NewParanoid(fs fs.FileSystem) Tar {
return &stiTar{
FileSystem: fs,
exclude: DefaultExclusionPattern,
timeout: defaultTimeout,
disallowOverwrite: true,
disallowOutsidePaths: true,
disallowSpecialFiles: true,
}
}
// NewParanoidWithTimeout creates a new Tar with the provided timeout extracting files.
// It has restrictions on what it can do while extracting files.
func NewParanoidWithTimeout(fs fs.FileSystem, timeout time.Duration) Tar {
return &stiTar{
FileSystem: fs,
exclude: DefaultExclusionPattern,
timeout: timeout,
disallowOverwrite: true,
disallowOutsidePaths: true,
disallowSpecialFiles: true,
}
}
// stiTar is an implementation of the Tar interface
type stiTar struct {
fs.FileSystem
timeout time.Duration
exclude *regexp.Regexp
includeDirInPath bool
disallowOverwrite bool
disallowOutsidePaths bool
disallowSpecialFiles bool
}
// SetExclusionPattern sets the exclusion pattern for tar creation. The
// exclusion pattern always uses UNIX-style (/) path separators, even on
// Windows.
func (t *stiTar) SetExclusionPattern(p *regexp.Regexp) {
t.exclude = p
}
// CreateTarFile creates a tar file from the given directory
// while excluding files that match the given exclusion pattern
// It returns the name of the created file
func (t *stiTar) CreateTarFile(base, dir string) (string, error) {
tarFile, err := ioutil.TempFile(base, "tar")
defer tarFile.Close()
if err != nil {
return "", err
}
if err = t.CreateTarStream(dir, false, tarFile); err != nil {
return "", err
}
return tarFile.Name(), nil
}
func (t *stiTar) shouldExclude(path string) bool {
return t.exclude != nil && t.exclude.String() != "" && t.exclude.MatchString(filepath.ToSlash(path))
}
// CreateTarStream calls CreateTarStreamToTarWriter with a nil logger
func (t *stiTar) CreateTarStream(dir string, includeDirInPath bool, writer io.Writer) error {
tarWriter := tar.NewWriter(writer)
defer tarWriter.Close()
return t.CreateTarStreamToTarWriter(dir, includeDirInPath, tarWriter, nil)
}
// CreateTarStreamReader returns an io.ReadCloser from which a tar stream can be
// read. The tar stream is created using CreateTarStream.
func (t *stiTar) CreateTarStreamReader(dir string, includeDirInPath bool) io.ReadCloser {
r, w := io.Pipe()
go func() {
w.CloseWithError(t.CreateTarStream(dir, includeDirInPath, w))
}()
return r
}
// CreateTarStreamToTarWriter creates a tar stream on the given writer from
// the given directory while excluding files that match the given
// exclusion pattern.
func (t *stiTar) CreateTarStreamToTarWriter(dir string, includeDirInPath bool, tarWriter Writer, logger io.Writer) error {
dir = filepath.Clean(dir) // remove relative paths and extraneous slashes
glog.V(5).Infof("Adding %q to tar ...", dir)
err := t.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// on Windows, directory symlinks report as a directory and as a symlink.
// They should be treated as symlinks.
if !t.shouldExclude(path) {
// if file is a link just writing header info is enough
if info.Mode()&os.ModeSymlink != 0 {
if dir == path {
return nil
}
if err = t.writeTarHeader(tarWriter, dir, path, info, includeDirInPath, logger); err != nil {
glog.Errorf("Error writing header for %q: %v", info.Name(), err)
}
// on Windows, filepath.Walk recurses into directory symlinks when it
// shouldn't. https://github.com/golang/go/issues/17540
if err == nil && info.Mode()&os.ModeDir != 0 {
return filepath.SkipDir
}
return err
}
if info.IsDir() {
if dir == path {
return nil
}
if err = t.writeTarHeader(tarWriter, dir, path, info, includeDirInPath, logger); err != nil {
glog.Errorf("Error writing header for %q: %v", info.Name(), err)
}
return err
}
// regular files are copied into tar, if accessible
file, err := os.Open(path)
if err != nil {
glog.Errorf("Ignoring file %s: %v", path, err)
return nil
}
defer file.Close()
if err = t.writeTarHeader(tarWriter, dir, path, info, includeDirInPath, logger); err != nil {
glog.Errorf("Error writing header for %q: %v", info.Name(), err)
return err
}
if _, err = io.Copy(tarWriter, file); err != nil {
glog.Errorf("Error copying file %q to tar: %v", path, err)
return err
}
}
return nil
})
if err != nil {
glog.Errorf("Error writing tar: %v", err)
return err
}
return nil
}
// writeTarHeader writes tar header for given file, returns error if operation fails
func (t *stiTar) writeTarHeader(tarWriter Writer, dir string, path string, info os.FileInfo, includeDirInPath bool, logger io.Writer) error {
var (
link string
err error
)
if info.Mode()&os.ModeSymlink != 0 {
link, err = os.Readlink(path)
if err != nil {
return err
}
}
header, err := tar.FileInfoHeader(info, link)
if err != nil {
return err
}
// on Windows, tar.FileInfoHeader incorrectly interprets directory symlinks
// as directories. https://github.com/golang/go/issues/17541
if info.Mode()&os.ModeSymlink != 0 && info.Mode()&os.ModeDir != 0 {
header.Typeflag = tar.TypeSymlink
header.Mode &^= 040000 // c_ISDIR
header.Mode |= 0120000 // c_ISLNK
header.Linkname = link
}
prefix := dir
if includeDirInPath {
prefix = filepath.Dir(prefix)
}
fileName := path
if prefix != "." {
fileName = path[1+len(prefix):]
}
header.Name = filepath.ToSlash(fileName)
header.Linkname = filepath.ToSlash(header.Linkname)
// Force the header format to PAX to support UTF-8 filenames
// and use the same format throughout the entire tar file.
header.Format = tar.FormatPAX
logFile(logger, header.Name)
glog.V(5).Infof("Adding to tar: %s as %s", path, header.Name)
return tarWriter.WriteHeader(header)
}
// ExtractTarStream calls ExtractTarStreamFromTarReader with a default reader and nil logger
func (t *stiTar) ExtractTarStream(dir string, reader io.Reader) error {
tarReader := tar.NewReader(reader)
return t.ExtractTarStreamFromTarReader(dir, tarReader, nil)
}
// ExtractTarStreamWithLogging calls ExtractTarStreamFromTarReader with a default reader
func (t *stiTar) ExtractTarStreamWithLogging(dir string, reader io.Reader, logger io.Writer) error {
tarReader := tar.NewReader(reader)
return t.ExtractTarStreamFromTarReader(dir, tarReader, logger)
}
// ExtractTarStreamFromTarReader extracts files from a given tar stream.
// Times out if reading from the stream for any given file
// exceeds the value of timeout
func (t *stiTar) ExtractTarStreamFromTarReader(dir string, tarReader Reader, logger io.Writer) error {
err := util.TimeoutAfter(t.timeout, "", func(timeoutTimer *time.Timer) error {
for {
header, err := tarReader.Next()
if !timeoutTimer.Stop() {
return &util.TimeoutError{}
}
timeoutTimer.Reset(t.timeout)
if err == io.EOF {
return nil
}
if err != nil {
glog.Errorf("Error reading next tar header: %v", err)
return err
}
if t.disallowSpecialFiles {
switch header.Typeflag {
case tar.TypeReg, tar.TypeRegA, tar.TypeLink, tar.TypeSymlink, tar.TypeDir, tar.TypeGNUSparse:
default:
glog.Warningf("Skipping special file %s, type: %v", header.Name, header.Typeflag)
continue
}
}
p := header.Name
if t.disallowOutsidePaths {
p = filepath.Clean(filepath.Join(dir, p))
if !strings.HasPrefix(p, dir) {
glog.Warningf("Skipping relative path file in tar: %s", header.Name)
continue
}
}
if header.FileInfo().IsDir() {
dirPath := filepath.Join(dir, filepath.Clean(header.Name))
glog.V(3).Infof("Creating directory %s", dirPath)
if err = os.MkdirAll(dirPath, 0700); err != nil {
glog.Errorf("Error creating dir %q: %v", dirPath, err)
return err
}
t.Chmod(dirPath, header.FileInfo().Mode())
} else {
fileDir := filepath.Dir(header.Name)
dirPath := filepath.Join(dir, filepath.Clean(fileDir))
glog.V(3).Infof("Creating directory %s", dirPath)
if err = os.MkdirAll(dirPath, 0700); err != nil {
glog.Errorf("Error creating dir %q: %v", dirPath, err)
return err
}
if header.Typeflag == tar.TypeSymlink {
if err := t.extractLink(dir, header, tarReader); err != nil {
glog.Errorf("Error extracting link %q: %v", header.Name, err)
return err
}
continue
}
logFile(logger, header.Name)
if err := t.extractFile(dir, header, tarReader); err != nil {
glog.Errorf("Error extracting file %q: %v", header.Name, err)
return err
}
}
}
})
if err != nil {
glog.Errorf("Error extracting tar stream: %v", err)
} else {
glog.V(2).Info("Done extracting tar stream")
}
if util.IsTimeoutError(err) {
err = s2ierr.NewTarTimeoutError()
}
return err
}
func (t *stiTar) extractLink(dir string, header *tar.Header, tarReader io.Reader) error {
dest := filepath.Join(dir, header.Name)
source := header.Linkname
if t.disallowOutsidePaths {
target := filepath.Clean(filepath.Join(dest, "..", source))
if !strings.HasPrefix(target, dir) {
glog.Warningf("Skipping symlink that points to relative path: %s", header.Linkname)
return nil
}
}
if t.disallowOverwrite {
if _, err := os.Stat(dest); !os.IsNotExist(err) {
glog.Warningf("Refusing to overwrite existing file: %s", dest)
return nil
}
}
glog.V(3).Infof("Creating symbolic link from %q to %q", dest, source)
// TODO: set mtime for symlink (unfortunately we can't use os.Chtimes() and probably should use syscall)
return os.Symlink(source, dest)
}
func (t *stiTar) extractFile(dir string, header *tar.Header, tarReader io.Reader) error {
path := filepath.Join(dir, header.Name)
if t.disallowOverwrite {
if _, err := os.Stat(path); !os.IsNotExist(err) {
glog.Warningf("Refusing to overwrite existing file: %s", path)
return nil
}
}
glog.V(3).Infof("Creating %s", path)
file, err := os.Create(path)
if err != nil {
return err
}
// The file times need to be modified after it's been closed thus this function
// is deferred after the file close (LIFO order for defer)
defer os.Chtimes(path, time.Now(), header.FileInfo().ModTime())
defer file.Close()
glog.V(3).Infof("Extracting/writing %s", path)
written, err := io.Copy(file, tarReader)
if err != nil {
return err
}
if written != header.Size {
return fmt.Errorf("wrote %d bytes, expected to write %d", written, header.Size)
}
return t.Chmod(path, header.FileInfo().Mode())
}
func logFile(logger io.Writer, name string) {
if logger == nil {
return
}
fmt.Fprintf(logger, "%s\n", name)
}
package util
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
// CallbackInvoker posts results to a callback URL when a STI build is done.
type CallbackInvoker interface {
ExecuteCallback(callbackURL string, success bool, labels map[string]string, messages []string) []string
}
// NewCallbackInvoker creates an instance of the default CallbackInvoker implementation
func NewCallbackInvoker() CallbackInvoker {
invoker := &callbackInvoker{}
invoker.postFunc = invoker.httpPost
return invoker
}
type callbackInvoker struct {
postFunc func(url, contentType string, body io.Reader) (resp *http.Response, err error)
}
// ExecuteCallback prepares a JSON payload and posts it to the specified callback URL
func (c *callbackInvoker) ExecuteCallback(callbackURL string, success bool, labels map[string]string, messages []string) []string {
buf := new(bytes.Buffer)
writer := bufio.NewWriter(buf)
data := map[string]interface{}{
"success": success,
}
if len(labels) > 0 {
data["labels"] = labels
}
jsonBuffer := new(bytes.Buffer)
writer = bufio.NewWriter(jsonBuffer)
jsonWriter := json.NewEncoder(writer)
jsonWriter.Encode(data)
writer.Flush()
var (
resp *http.Response
err error
)
for retries := 0; retries < 3; retries++ {
resp, err = c.postFunc(callbackURL, "application/json", jsonBuffer)
if err != nil {
errorMessage := fmt.Sprintf("Unable to invoke callback: %v", err)
messages = append(messages, errorMessage)
}
if resp != nil {
if resp.StatusCode >= 300 {
errorMessage := fmt.Sprintf("Callback returned with error code: %d", resp.StatusCode)
messages = append(messages, errorMessage)
}
break
}
}
return messages
}
func (*callbackInvoker) httpPost(url, contentType string, body io.Reader) (resp *http.Response, err error) {
return http.Post(url, contentType, body)
}
package util
import (
"bufio"
"fmt"
"net/url"
"os"
"regexp"
"strings"
)
// case insensitively match all key=value variables containing the word "proxy"
var proxyRegex = regexp.MustCompile("(?i).*proxy.*")
// ReadEnvironmentFile reads the content for a file that contains a list of
// environment variables and values. The key-pairs are separated by a new line
// character. The file can also have comments (both '#' and '//' are supported).
func ReadEnvironmentFile(path string) (map[string]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
result := map[string]string{}
scanner := bufio.NewScanner(f)
for scanner.Scan() {
s := strings.TrimSpace(scanner.Text())
// Allow for comments in environment file
if strings.HasPrefix(s, "#") || strings.HasPrefix(s, "//") {
continue
}
parts := strings.SplitN(s, "=", 2)
if len(parts) != 2 {
continue
}
result[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
return result, scanner.Err()
}
// SafeForLoggingEnv attempts to strip sensitive information from proxy
// environment variable strings in key=value form.
func SafeForLoggingEnv(env []string) []string {
newEnv := make([]string, len(env))
copy(newEnv, env)
for i, entry := range newEnv {
parts := strings.SplitN(entry, "=", 2)
if !proxyRegex.MatchString(parts[0]) {
continue
}
newVal, _ := SafeForLoggingURL(parts[1])
newEnv[i] = fmt.Sprintf("%s=%s", parts[0], newVal)
}
return newEnv
}
// SafeForLoggingURL removes the user:password section of
// a url if present. If not present or the value is unparseable,
// the value is returned unchanged.
func SafeForLoggingURL(input string) (string, error) {
u, err := url.Parse(input)
if err != nil {
return input, err
}
if u.User == nil {
return input, nil
}
if _, passwordSet := u.User.Password(); passwordSet {
// wipe out the user info from the url.
u.User = url.User("redacted")
}
return u.String(), nil
}
package fs
import (
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"sync"
"time"
utilglog "github.com/openshift/source-to-image/pkg/util/glog"
s2ierr "github.com/openshift/source-to-image/pkg/errors"
)
var glog = utilglog.StderrLog
// FileSystem allows STI to work with the file system and
// perform tasks such as creating and deleting directories
type FileSystem interface {
Chmod(file string, mode os.FileMode) error
Rename(from, to string) error
MkdirAll(dirname string) error
MkdirAllWithPermissions(dirname string, perm os.FileMode) error
Mkdir(dirname string) error
Exists(file string) bool
Copy(sourcePath, targetPath string, filesToIgnore map[string]string) error
CopyContents(sourcePath, targetPath string, filesToIgnore map[string]string) error
RemoveDirectory(dir string) error
CreateWorkingDirectory() (string, error)
Open(file string) (io.ReadCloser, error)
Create(file string) (io.WriteCloser, error)
WriteFile(file string, data []byte) error
ReadDir(string) ([]os.FileInfo, error)
Stat(string) (os.FileInfo, error)
Lstat(string) (os.FileInfo, error)
Walk(string, filepath.WalkFunc) error
Readlink(string) (string, error)
Symlink(string, string) error
KeepSymlinks(bool)
ShouldKeepSymlinks() bool
}
// NewFileSystem creates a new instance of the default FileSystem
// implementation
func NewFileSystem() FileSystem {
return &fs{
fileModes: make(map[string]os.FileMode),
keepSymlinks: false,
}
}
type fs struct {
// on Windows, fileModes is used to track the UNIX file mode of every file we
// work with; m is used to synchronize access to fileModes.
fileModes map[string]os.FileMode
m sync.Mutex
keepSymlinks bool
}
// FileInfo is a struct which implements os.FileInfo. We use it (a) for test
// purposes, and (b) because we enrich the FileMode on Windows systems
type FileInfo struct {
FileName string
FileSize int64
FileMode os.FileMode
FileModTime time.Time
FileIsDir bool
FileSys interface{}
}
// Name retuns the filename of fi
func (fi *FileInfo) Name() string {
return fi.FileName
}
// Size returns the file size of fi
func (fi *FileInfo) Size() int64 {
return fi.FileSize
}
// Mode returns the file mode of fi
func (fi *FileInfo) Mode() os.FileMode {
return fi.FileMode
}
// ModTime returns the file modification time of fi
func (fi *FileInfo) ModTime() time.Time {
return fi.FileModTime
}
// IsDir returns true if fi refers to a directory
func (fi *FileInfo) IsDir() bool {
return fi.FileIsDir
}
// Sys returns the sys interface of fi
func (fi *FileInfo) Sys() interface{} {
return fi.FileSys
}
func copyFileInfo(src os.FileInfo) *FileInfo {
return &FileInfo{
FileName: src.Name(),
FileSize: src.Size(),
FileMode: src.Mode(),
FileModTime: src.ModTime(),
FileIsDir: src.IsDir(),
FileSys: src.Sys(),
}
}
// Stat returns a FileInfo describing the named file.
func (h *fs) Stat(path string) (os.FileInfo, error) {
fi, err := os.Stat(path)
if runtime.GOOS == "windows" && err == nil {
fi = h.enrichFileInfo(path, fi)
}
return fi, err
}
// Lstat returns a FileInfo describing the named file (not following symlinks).
func (h *fs) Lstat(path string) (os.FileInfo, error) {
fi, err := os.Lstat(path)
if runtime.GOOS == "windows" && err == nil {
fi = h.enrichFileInfo(path, fi)
}
return fi, err
}
// ReadDir reads the directory named by dirname and returns a list of directory
// entries sorted by filename.
func (h *fs) ReadDir(path string) ([]os.FileInfo, error) {
fis, err := ioutil.ReadDir(path)
if runtime.GOOS == "windows" && err == nil {
h.enrichFileInfos(path, fis)
}
return fis, err
}
// Chmod sets the file mode
func (h *fs) Chmod(file string, mode os.FileMode) error {
err := os.Chmod(file, mode)
if runtime.GOOS == "windows" && err == nil {
h.m.Lock()
h.fileModes[file] = mode
h.m.Unlock()
return nil
}
return err
}
// Rename renames or moves a file
func (h *fs) Rename(from, to string) error {
return os.Rename(from, to)
}
// MkdirAll creates the directory and all its parents
func (h *fs) MkdirAll(dirname string) error {
return os.MkdirAll(dirname, 0700)
}
// MkdirAllWithPermissions creates the directory and all its parents with the provided permissions
func (h *fs) MkdirAllWithPermissions(dirname string, perm os.FileMode) error {
return os.MkdirAll(dirname, perm)
}
// Mkdir creates the specified directory
func (h *fs) Mkdir(dirname string) error {
return os.Mkdir(dirname, 0700)
}
// Exists determines whether the given file exists
func (h *fs) Exists(file string) bool {
_, err := h.Stat(file)
return err == nil
}
// Copy copies the source to a destination.
// If the source is a file, then the destination has to be a file as well,
// otherwise you will get an error.
// If the source is a directory, then the destination has to be a directory and
// we copy the content of the source directory to destination directory
// recursively.
func (h *fs) Copy(source string, dest string, filesToIgnore map[string]string) (err error) {
return doCopy(h, source, dest, filesToIgnore)
}
// KeepSymlinks configures fs to copy symlinks from src as symlinks to dst.
// Default behavior is to follow symlinks and copy files by content.
func (h *fs) KeepSymlinks(k bool) {
h.keepSymlinks = k
}
// ShouldKeepSymlinks is exported only due to the design of fs util package
// and how the tests are structured. It indicates whether the implementation
// should copy symlinks as symlinks or follow symlinks and copy by content.
func (h *fs) ShouldKeepSymlinks() bool {
return h.keepSymlinks
}
// If src is symlink and symlink copy has been enabled, copy as a symlink.
// Otherwise ignore symlink and let rest of the code follow the symlink
// and copy the content of the file
func handleSymlink(h FileSystem, source, dest string) (bool, error) {
lstatinfo, lstaterr := h.Lstat(source)
_, staterr := h.Stat(source)
if lstaterr == nil &&
lstatinfo.Mode()&os.ModeSymlink != 0 {
if os.IsNotExist(staterr) {
glog.V(5).Infof("(broken) L %q -> %q", source, dest)
} else if h.ShouldKeepSymlinks() {
glog.V(5).Infof("L %q -> %q", source, dest)
} else {
// symlink not handled here, will copy the file content
return false, nil
}
linkdest, err := h.Readlink(source)
if err != nil {
return true, err
}
return true, h.Symlink(linkdest, dest)
}
// symlink not handled here, will copy the file content
return false, nil
}
func doCopy(h FileSystem, source, dest string, filesToIgnore map[string]string) error {
if handled, err := handleSymlink(h, source, dest); handled || err != nil {
return err
}
sourcefile, err := h.Open(source)
if err != nil {
return err
}
defer sourcefile.Close()
sourceinfo, err := h.Stat(source)
if err != nil {
return err
}
if sourceinfo.IsDir() {
_, ok := filesToIgnore[source]
if ok {
glog.V(5).Infof("Directory %q ignored", source)
return nil
}
glog.V(5).Infof("D %q -> %q", source, dest)
return h.CopyContents(source, dest, filesToIgnore)
}
destinfo, _ := h.Stat(dest)
if destinfo != nil && destinfo.IsDir() {
return fmt.Errorf("destination must be full path to a file, not directory")
}
destfile, err := h.Create(dest)
if err != nil {
return err
}
defer destfile.Close()
_, ok := filesToIgnore[source]
if ok {
glog.V(5).Infof("File %q ignored", source)
return nil
}
glog.V(5).Infof("F %q -> %q", source, dest)
if _, err := io.Copy(destfile, sourcefile); err != nil {
return err
}
return h.Chmod(dest, sourceinfo.Mode())
}
// CopyContents copies the content of the source directory to a destination
// directory.
// If the destination directory does not exists, it will be created.
// The source directory itself will not be copied, only its content. If you
// want this behavior, the destination must include the source directory name.
// It will skip any files provided in filesToIgnore from being copied
func (h *fs) CopyContents(src string, dest string, filesToIgnore map[string]string) (err error) {
sourceinfo, err := h.Stat(src)
if err != nil {
return err
}
if err = os.MkdirAll(dest, sourceinfo.Mode()); err != nil {
return err
}
directory, err := os.Open(src)
if err != nil {
return err
}
defer directory.Close()
objects, err := directory.Readdir(-1)
if err != nil {
return err
}
for _, obj := range objects {
source := path.Join(src, obj.Name())
destination := path.Join(dest, obj.Name())
if err := h.Copy(source, destination, filesToIgnore); err != nil {
return err
}
}
return
}
// RemoveDirectory removes the specified directory and all its contents
func (h *fs) RemoveDirectory(dir string) error {
glog.V(2).Infof("Removing directory '%s'", dir)
// HACK: If deleting a directory in windows, call out to the system to do the deletion
// TODO: Remove this workaround when we switch to go 1.7 -- os.RemoveAll should
// be fixed for Windows in that release. https://github.com/golang/go/issues/9606
if runtime.GOOS == "windows" {
command := exec.Command("cmd.exe", "/c", fmt.Sprintf("rd /s /q %s", dir))
output, err := command.Output()
if err != nil {
glog.Errorf("Error removing directory %q: %v %s", dir, err, string(output))
return err
}
return nil
}
err := os.RemoveAll(dir)
if err != nil {
glog.Errorf("Error removing directory '%s': %v", dir, err)
}
return err
}
// CreateWorkingDirectory creates a directory to be used for STI
func (h *fs) CreateWorkingDirectory() (directory string, err error) {
directory, err = ioutil.TempDir("", "s2i")
if err != nil {
return "", s2ierr.NewWorkDirError(directory, err)
}
return directory, err
}
// Open opens a file and returns a ReadCloser interface to that file
func (h *fs) Open(filename string) (io.ReadCloser, error) {
return os.Open(filename)
}
// Create creates a file and returns a WriteCloser interface to that file
func (h *fs) Create(filename string) (io.WriteCloser, error) {
return os.Create(filename)
}
// WriteFile opens a file and writes data to it, returning error if such
// occurred
func (h *fs) WriteFile(filename string, data []byte) error {
return ioutil.WriteFile(filename, data, 0700)
}
// Walk walks the file tree rooted at root, calling walkFn for each file or
// directory in the tree, including root.
func (h *fs) Walk(root string, walkFn filepath.WalkFunc) error {
wrapper := func(path string, info os.FileInfo, err error) error {
if runtime.GOOS == "windows" && err == nil {
info = h.enrichFileInfo(path, info)
}
return walkFn(path, info, err)
}
return filepath.Walk(root, wrapper)
}
// enrichFileInfo is used on Windows. It takes an os.FileInfo object, e.g. as
// returned by os.Stat, and enriches the OS-returned file mode with the "real"
// UNIX file mode, if we know what it is.
func (h *fs) enrichFileInfo(path string, fi os.FileInfo) os.FileInfo {
h.m.Lock()
if mode, ok := h.fileModes[path]; ok {
fi = copyFileInfo(fi)
fi.(*FileInfo).FileMode = mode
}
h.m.Unlock()
return fi
}
// enrichFileInfos is used on Windows. It takes an array of os.FileInfo
// objects, e.g. as returned by os.ReadDir, and for each file enriches the OS-
// returned file mode with the "real" UNIX file mode, if we know what it is.
func (h *fs) enrichFileInfos(root string, fis []os.FileInfo) {
h.m.Lock()
for i := range fis {
if mode, ok := h.fileModes[filepath.Join(root, fis[i].Name())]; ok {
fis[i] = copyFileInfo(fis[i])
fis[i].(*FileInfo).FileMode = mode
}
}
h.m.Unlock()
}
// Readlink reads the destination of a symlink
func (h *fs) Readlink(name string) (string, error) {
return os.Readlink(name)
}
// Symlink creates a symlink at newname, pointing to oldname
func (h *fs) Symlink(oldname, newname string) error {
return os.Symlink(oldname, newname)
}
package util
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/util/fs"
)
// FixInjectionsWithRelativePath fixes the injections that does not specify the
// destination directory or the directory is relative to use the provided
// working directory.
func FixInjectionsWithRelativePath(workdir string, injections api.VolumeList) api.VolumeList {
if len(injections) == 0 {
return injections
}
newList := api.VolumeList{}
for _, injection := range injections {
changed := false
if filepath.Clean(filepath.FromSlash(injection.Destination)) == "." {
injection.Destination = filepath.ToSlash(workdir)
changed = true
}
if filepath.ToSlash(injection.Destination)[0] != '/' {
injection.Destination = filepath.ToSlash(filepath.Join(workdir, injection.Destination))
changed = true
}
if changed {
glog.V(5).Infof("Using %q as a destination for injecting %q", injection.Destination, injection.Source)
}
newList = append(newList, injection)
}
return newList
}
// ListFilesToTruncate returns a flat list of all files that are injected into a
// container which need to be truncated. All files from nested directories are returned in the list.
func ListFilesToTruncate(fs fs.FileSystem, injections api.VolumeList) ([]string, error) {
result := []string{}
for _, s := range injections {
if s.Keep {
continue
}
files, err := ListFiles(fs, s)
if err != nil {
return nil, err
}
result = append(result, files...)
}
return result, nil
}
// ListFiles returns a flat list of all files injected into a container for the given `VolumeSpec`.
func ListFiles(fs fs.FileSystem, spec api.VolumeSpec) ([]string, error) {
result := []string{}
if _, err := os.Stat(spec.Source); err != nil {
return nil, err
}
err := fs.Walk(spec.Source, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
// Detected files will be truncated. k8s' AtomicWriter creates
// directories and symlinks to directories in order to inject files.
// An attempt to truncate either a dir or symlink to a dir will fail.
// Thus, we need to dereference symlinks to see if they might point
// to a directory.
// Do not try to simplify this logic to simply return nil if a symlink
// is detected. During the tar transfer to an assemble image, symlinked
// files are turned concrete (i.e. they will be turned into regular files
// containing the content of their target). These newly concrete files
// need to be truncated as well.
if f.Mode()&os.ModeSymlink != 0 {
linkDest, err := filepath.EvalSymlinks(path)
if err != nil {
return fmt.Errorf("unable to evaluate symlink [%v]: %v", path, err)
}
// Evaluate the destination of the link.
f, err = os.Lstat(linkDest)
if err != nil {
// This is not a fatal error. If AtomicWrite tried multiple times, a symlink might not point
// to a valid destination.
glog.Warningf("Unable to lstat symlink destination [%s]->[%s]. err: %v. Partial atomic write?", path, linkDest, err)
return nil
}
}
if f.IsDir() {
return nil
}
newPath := filepath.ToSlash(filepath.Join(spec.Destination, strings.TrimPrefix(path, spec.Source)))
result = append(result, newPath)
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
// CreateTruncateFilesScript creates a shell script that contains truncation
// of all files we injected into the container. The path to the script is returned.
// When the scriptName is provided, it is also truncated together with all
// secrets.
func CreateTruncateFilesScript(files []string, scriptName string) (string, error) {
rmScript := "set -e\n"
for _, s := range files {
rmScript += fmt.Sprintf("truncate -s0 %q\n", s)
}
f, err := ioutil.TempFile("", "s2i-injection-remove")
if err != nil {
return "", err
}
if len(scriptName) > 0 {
rmScript += fmt.Sprintf("truncate -s0 %q\n", scriptName)
}
rmScript += "set +e\n"
err = ioutil.WriteFile(f.Name(), []byte(rmScript), 0700)
return f.Name(), err
}
// CreateInjectionResultFile creates a result file with the message from the provided injection
// error. The path to the result file is returned. If the provided error is nil, an empty file is
// created.
func CreateInjectionResultFile(injectErr error) (string, error) {
f, err := ioutil.TempFile("", "s2i-injection-result")
if err != nil {
return "", err
}
if injectErr != nil {
err = ioutil.WriteFile(f.Name(), []byte(injectErr.Error()), 0700)
}
return f.Name(), err
}
// HandleInjectionError handles the error caused by injection and provide
// reasonable suggestion to users.
func HandleInjectionError(p api.VolumeSpec, err error) error {
if err == nil {
return nil
}
if strings.Contains(err.Error(), "no such file or directory") {
glog.Errorf("The destination directory for %q injection must exist in container (%q)", p.Source, p.Destination)
return err
}
glog.Errorf("Error occurred during injecting %q to %q: %v", p.Source, p.Destination, err)
return err
}
package util
import (
"fmt"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/api/constants"
"github.com/openshift/source-to-image/pkg/scm/git"
)
// GenerateOutputImageLabels generate the labels based on the s2i Config
// and source repository informations.
func GenerateOutputImageLabels(info *git.SourceInfo, config *api.Config) map[string]string {
labels := map[string]string{}
namespace := constants.DefaultNamespace
if len(config.LabelNamespace) > 0 {
namespace = config.LabelNamespace
}
labels = GenerateLabelsFromConfig(labels, config, namespace)
labels = GenerateLabelsFromSourceInfo(labels, info, namespace)
return labels
}
// GenerateLabelsFromConfig generate the labels based on build s2i Config
func GenerateLabelsFromConfig(labels map[string]string, config *api.Config, namespace string) map[string]string {
if len(config.Description) > 0 {
labels[constants.KubernetesDescriptionLabel] = config.Description
}
if len(config.DisplayName) > 0 {
labels[constants.KubernetesDisplayNameLabel] = config.DisplayName
} else if len(config.Tag) > 0 {
labels[constants.KubernetesDisplayNameLabel] = config.Tag
}
addBuildLabel(labels, "image", config.BuilderImage, namespace)
return labels
}
// GenerateLabelsFromSourceInfo generate the labels based on the source repository
// informations.
func GenerateLabelsFromSourceInfo(labels map[string]string, info *git.SourceInfo, namespace string) map[string]string {
if info == nil {
glog.V(3).Info("Unable to fetch source information, the output image labels will not be set")
return labels
}
if len(info.AuthorName) > 0 {
author := fmt.Sprintf("%s <%s>", info.AuthorName, info.AuthorEmail)
addBuildLabel(labels, "commit.author", author, namespace)
}
addBuildLabel(labels, "commit.date", info.Date, namespace)
addBuildLabel(labels, "commit.id", info.CommitID, namespace)
addBuildLabel(labels, "commit.ref", info.Ref, namespace)
addBuildLabel(labels, "commit.message", info.Message, namespace)
addBuildLabel(labels, "source-location", info.Location, namespace)
addBuildLabel(labels, "source-context-dir", info.ContextDir, namespace)
return labels
}
// addBuildLabel adds a new "*.build.*" label into map when the
// value of this label is not empty
func addBuildLabel(to map[string]string, key, value, namespace string) {
if len(value) == 0 {
return
}
to[namespace+"build."+key] = value
}
package status
import (
"github.com/openshift/source-to-image/pkg/api"
)
const (
// ReasonAssembleFailed is the reason associated with the Assemble script
// failing.
ReasonAssembleFailed api.StepFailureReason = "AssembleFailed"
// ReasonMessageAssembleFailed is the message associated with the Assemble
// script failing.
ReasonMessageAssembleFailed api.StepFailureMessage = "Assemble script failed."
// ReasonPullBuilderImageFailed is the reason associated with failing to pull
// the builder image.
ReasonPullBuilderImageFailed api.StepFailureReason = "PullBuilderImageFailed"
// ReasonMessagePullBuilderImageFailed is the message associated with failing
// to pull the builder image.
ReasonMessagePullBuilderImageFailed api.StepFailureMessage = "Failed to pull builder image."
// ReasonPullRuntimeImageFailed is the reason associated with failing to pull
// the runtime image.
ReasonPullRuntimeImageFailed api.StepFailureReason = "PullRuntimeImageFailed"
// ReasonMessagePullRuntimeImageFailed is the message associated with failing
// to pull the runtime image.
ReasonMessagePullRuntimeImageFailed api.StepFailureMessage = "Failed to pull runtime image."
// ReasonPullPreviousImageFailed is the reason associated with failing to
// pull the previous image.
ReasonPullPreviousImageFailed api.StepFailureReason = "PullPreviousImageFailed"
// ReasonMessagePullPreviousImageFailed is the message associated with
// failing to pull the previous image.
ReasonMessagePullPreviousImageFailed api.StepFailureMessage = "Failed to pull the previous image for incremental build."
// ReasonCommitContainerFailed is the reason associated with failing to
// commit the container to the final image.
ReasonCommitContainerFailed api.StepFailureReason = "ContainerCommitFailed"
// ReasonMessageCommitContainerFailed is the message associated with failing to
// commit the container to the final image.
ReasonMessageCommitContainerFailed api.StepFailureMessage = "Failed to commit container."
// ReasonFetchSourceFailed is the reason associated with failing to download
// the source of the build.
ReasonFetchSourceFailed api.StepFailureReason = "FetchSourceFailed"
// ReasonMessageFetchSourceFailed is the message associated with failing to download
// the source of the build.
ReasonMessageFetchSourceFailed api.StepFailureMessage = "Failed to fetch source for build."
// ReasonDockerImageBuildFailed is the reason associated with a failed
// Docker image build.
ReasonDockerImageBuildFailed api.StepFailureReason = "DockerImageBuildFailed"
// ReasonMessageDockerImageBuildFailed is the message associated with a failed
// Docker image build.
ReasonMessageDockerImageBuildFailed api.StepFailureMessage = "Docker image build failed."
// ReasonDockerfileCreateFailed is the reason associated with failing to create a
// Dockerfile for a build.
ReasonDockerfileCreateFailed api.StepFailureReason = "DockerFileCreationFailed"
// ReasonMessageDockerfileCreateFailed is the message associated with failing to create a
// Dockerfile for a build.
ReasonMessageDockerfileCreateFailed api.StepFailureMessage = "Failed to create Dockerfile."
// ReasonInvalidArtifactsMapping is the reason associated with an
// invalid artifacts mapping of files that need to be copied.
ReasonInvalidArtifactsMapping api.StepFailureReason = "InvalidArtifactsMapping"
// ReasonMessageInvalidArtifactsMapping is the message associated with an
// invalid artifacts mapping of files that need to be copied.
ReasonMessageInvalidArtifactsMapping api.StepFailureMessage = "Invalid artifacts mapping specified."
// ReasonScriptsFetchFailed is the reason associated with a failure to
// download specified scripts in the application image.
ReasonScriptsFetchFailed api.StepFailureReason = "FetchScriptsFailed"
// ReasonMessageScriptsFetchFailed is the message associated with a failure to
// download specified scripts in the application image.
ReasonMessageScriptsFetchFailed api.StepFailureMessage = "Failed to fetch specified scripts."
// ReasonRuntimeArtifactsFetchFailed is the reason associated with a failure
// to download the specified runtime scripts.
ReasonRuntimeArtifactsFetchFailed api.StepFailureReason = "FetchRuntimeArtifactsFailed"
// ReasonMessageRuntimeArtifactsFetchFailed is the message associated with a
// failure to download the specified runtime scripts in the application
// image.
ReasonMessageRuntimeArtifactsFetchFailed api.StepFailureMessage = "Failed to fetch specified runtime artifacts."
// ReasonFSOperationFailed is the reason associated with a failed fs
// operation. Create, remove directory, copy file, etc.
ReasonFSOperationFailed api.StepFailureReason = "FileSystemOperationFailed"
// ReasonMessageFSOperationFailed is the message associated with a failed fs
// operation. Create, remove directory, copy file, etc.
ReasonMessageFSOperationFailed api.StepFailureMessage = "Failed to perform filesystem operation."
// ReasonInstallScriptsFailed is the reason associated with a failure to
// install scripts in the builder image.
ReasonInstallScriptsFailed api.StepFailureReason = "InstallScriptsFailed"
// ReasonMessageInstallScriptsFailed is the message associated with a failure to
// install scripts in the builder image.
ReasonMessageInstallScriptsFailed api.StepFailureMessage = "Failed to install specified scripts."
// ReasonGenericS2IBuildFailed is the reason associated with a broad range of
// failures.
ReasonGenericS2IBuildFailed api.StepFailureReason = "GenericS2IBuildFailed"
// ReasonMessageGenericS2iBuildFailed is the message associated with a broad
// range of failures.
ReasonMessageGenericS2iBuildFailed api.StepFailureMessage = "Generic S2I Build failure - check S2I logs for details."
// ReasonOnBuildForbidden is the failure reason associated with an image that
// uses the ONBUILD instruction when it's not allowed.
ReasonOnBuildForbidden api.StepFailureReason = "OnBuildForbidden"
// ReasonMessageOnBuildForbidden is the message associated with an image that
// uses the ONBUILD instruction when it's not allowed.
ReasonMessageOnBuildForbidden api.StepFailureMessage = "ONBUILD instructions not allowed in this context."
// ReasonAssembleUserForbidden is the failure reason associated with an image that
// uses a forbidden AssembleUser.
ReasonAssembleUserForbidden api.StepFailureReason = "AssembleUserForbidden"
// ReasonMessageAssembleUserForbidden is the failure reason associated with an image that
// uses a forbidden AssembleUser.
ReasonMessageAssembleUserForbidden api.StepFailureMessage = "Assemble user for S2I build is forbidden."
)
// NewFailureReason initializes a new failure reason that contains both the
// reason and a message to be displayed.
func NewFailureReason(reason api.StepFailureReason, message api.StepFailureMessage) api.FailureReason {
return api.FailureReason{
Reason: reason,
Message: message,
}
}
package util
// Includes determines if the given string is in the provided slice of strings.
func Includes(arr []string, str string) bool {
for _, s := range arr {
if s == str {
return true
}
}
return false
}
// FirstNonEmpty returns the first non-empty string in the provided list of strings.
func FirstNonEmpty(args ...string) string {
for _, value := range args {
if len(value) > 0 {
return value
}
}
return ""
}
package util
import (
"fmt"
"time"
)
// TimeoutError is error returned after timeout occurred.
type TimeoutError struct {
after time.Duration
message string
}
// Error implements the Go error interface.
func (t *TimeoutError) Error() string {
if len(t.message) > 0 {
return fmt.Sprintf("%s timed out after %v", t.message, t.after)
}
return fmt.Sprintf("function timed out after %v", t.after)
}
// TimeoutAfter executes the provided function and returns TimeoutError in the
// case that the execution time of the function exceeded the provided duration.
// The provided function is passed the timer in case it wishes to reset it. If
// so, the following pattern must be used:
// if !timer.Stop() {
// return &TimeoutError{}
// }
// timer.Reset(timeout)
func TimeoutAfter(t time.Duration, errorMsg string, f func(*time.Timer) error) error {
c := make(chan error, 1)
timer := time.NewTimer(t)
go func() {
err := f(timer)
if !IsTimeoutError(err) {
c <- err
}
}()
select {
case err := <-c:
timer.Stop()
return err
case <-timer.C:
return &TimeoutError{after: t, message: errorMsg}
}
}
// IsTimeoutError checks if the provided error is a TimeoutError.
func IsTimeoutError(e error) bool {
_, ok := e.(*TimeoutError)
return ok
}
package user
import (
"errors"
"fmt"
"strconv"
"strings"
)
// Range errors
var (
ErrInvalidRange = errors.New("invalid range; a range must consist of positive integers and the upper bound must be greater than or equal to the lower bound")
)
// ErrParseRange is an error encountered while parsing a Range
type ErrParseRange struct {
cause error
}
func (e *ErrParseRange) Error() string {
msg := "error parsing range; a range must be of one of the following formats: [n], [-n], [n-], [n:m] where n and m are positive numbers and m is greater than or equal to n"
if e.cause != nil {
msg = fmt.Sprintf("%s: %v", msg, e.cause)
}
return msg
}
// Range represents a range of user ids. It can be unbound at either end with a nil value
// I both From and To are present, To must be greater than or equal to From. Bounds are inclusive
type Range struct {
from *int
to *int
}
// NewRange creates a new range with lower and upper bound
func NewRange(from int, to int) (*Range, error) {
return (&rangeBuilder{}).from(from, nil).to(to, nil).Range()
}
// NewRangeTo creates a new range with only the upper bound
func NewRangeTo(to int) (*Range, error) {
return (&rangeBuilder{}).to(to, nil).Range()
}
// NewRangeFrom creates a new range with only the lower bound
func NewRangeFrom(from int) (*Range, error) {
return (&rangeBuilder{}).from(from, nil).Range()
}
func parseInt(str string) (int, error) {
num, err := strconv.Atoi(str)
if err != nil {
return 0, &ErrParseRange{cause: err}
}
return num, nil
}
type rangeBuilder struct {
r Range
err error
}
func (b *rangeBuilder) from(num int, err error) *rangeBuilder {
return b.setBound(num, err, &b.r.from)
}
func (b *rangeBuilder) to(num int, err error) *rangeBuilder {
return b.setBound(num, err, &b.r.to)
}
func (b *rangeBuilder) setBound(num int, err error, bound **int) *rangeBuilder {
if b.err != nil {
return b
}
if b.err = err; b.err != nil {
return b
}
if num < 0 {
b.err = ErrInvalidRange
return b
}
*bound = &num
return b
}
// Range returns the completed Range from the rangeBuilder.
func (b *rangeBuilder) Range() (*Range, error) {
if b.err != nil {
return nil, b.err
}
if b.r.from != nil && b.r.to != nil && *b.r.to < *b.r.from {
return nil, ErrInvalidRange
}
return &b.r, nil
}
// ParseRange creates a Range from a given string
func ParseRange(value string) (*Range, error) {
value = strings.TrimSpace(value)
b := &rangeBuilder{}
if value == "" {
return b.Range()
}
parts := strings.Split(value, "-")
switch len(parts) {
case 1:
num, err := parseInt(parts[0])
return b.from(num, err).to(num, err).Range()
case 2:
if parts[0] != "" {
b.from(parseInt(parts[0]))
}
if parts[1] != "" {
b.to(parseInt(parts[1]))
}
return b.Range()
default:
return nil, &ErrParseRange{}
}
}
// Contains returns true if the argument falls inside the Range
func (r *Range) Contains(value int) bool {
if r.from == nil && r.to == nil {
return false
}
if r.from != nil && value < *r.from {
return false
}
if r.to != nil && value > *r.to {
return false
}
return true
}
// String returns a parse-able string representation of a Range
func (r *Range) String() string {
switch {
case r.from == nil && r.to == nil:
return ""
case r.from == nil:
return fmt.Sprintf("-%d", *r.to)
case r.to == nil:
return fmt.Sprintf("%d-", *r.from)
case *r.from == *r.to:
return fmt.Sprintf("%d", *r.to)
default:
return fmt.Sprintf("%d-%d", *r.from, *r.to)
}
}
// Type returns the type of a Range object
func (r *Range) Type() string {
return "user.Range"
}
// Set sets the value of a Range object
func (r *Range) Set(value string) error {
newRange, err := ParseRange(value)
if err != nil {
return err
}
*r = *newRange
return nil
}
// Empty returns true if the range has no bounds
func (r *Range) Empty() bool {
return r.from == nil && r.to == nil
}
package user
import (
"strconv"
"strings"
)
// RangeList is a list of user ranges
type RangeList []*Range
// ParseRangeList parses a string that contains a comma-separated list of ranges
func ParseRangeList(str string) (*RangeList, error) {
rl := RangeList{}
if len(str) == 0 {
return &rl, nil
}
parts := strings.Split(str, ",")
for _, p := range parts {
r, err := ParseRange(p)
if err != nil {
return nil, err
}
rl = append(rl, r)
}
return &rl, nil
}
// Empty returns true if the RangeList is empty
func (l *RangeList) Empty() bool {
if len(*l) == 0 {
return true
}
for _, r := range *l {
if !r.Empty() {
return false
}
}
return true
}
// Contains returns true if the uid is contained by any range in the RangeList
func (l *RangeList) Contains(uid int) bool {
for _, r := range *l {
if r.Contains(uid) {
return true
}
}
return false
}
// Type returns the type of a RangeList object
func (l *RangeList) Type() string {
return "user.RangeList"
}
// Set sets the value of a RangeList object
func (l *RangeList) Set(value string) error {
newRangeList, err := ParseRangeList(value)
if err != nil {
return err
}
*l = *newRangeList
return nil
}
// String returns a parseable string representation of a RangeList
func (l *RangeList) String() string {
rangeStrings := []string{}
for _, r := range *l {
rangeStrings = append(rangeStrings, r.String())
}
return strings.Join(rangeStrings, ",")
}
// IsUserAllowed checks that the given user is numeric and is
// contained by the given RangeList. Returns true if
// allowed is nil or empty.
func IsUserAllowed(user string, allowed *RangeList) bool {
if allowed == nil || allowed.Empty() {
return true
}
uid, err := strconv.Atoi(user)
if err != nil {
return false
}
return allowed.Contains(uid)
}
package util
import (
"github.com/docker/docker/api/types/container"
utilglog "github.com/openshift/source-to-image/pkg/util/glog"
)
var glog = utilglog.StderrLog
// SafeForLoggingContainerConfig returns a copy of the container.Config object
// with sensitive information (proxy environment variables containing credentials)
// redacted.
func SafeForLoggingContainerConfig(config *container.Config) *container.Config {
strippedEnv := SafeForLoggingEnv(config.Env)
newConfig := *config
newConfig.Env = strippedEnv
return &newConfig
}