forked from openshift/origin
-
Notifications
You must be signed in to change notification settings - Fork 0
/
layered.go
218 lines (196 loc) · 7 KB
/
layered.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
package layered
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"time"
"github.com/golang/glog"
"github.com/openshift/source-to-image/pkg/api"
"github.com/openshift/source-to-image/pkg/build"
"github.com/openshift/source-to-image/pkg/docker"
"github.com/openshift/source-to-image/pkg/errors"
"github.com/openshift/source-to-image/pkg/tar"
"github.com/openshift/source-to-image/pkg/util"
)
const defaultDestination = "/tmp"
type Layered struct {
config *api.Config
docker docker.Docker
fs util.FileSystem
tar tar.Tar
scripts build.ScriptsHandler
}
func New(config *api.Config, scripts build.ScriptsHandler, overrides build.Overrides) (*Layered, error) {
d, err := docker.New(config.DockerConfig, config.PullAuthentication)
if err != nil {
return nil, err
}
return &Layered{
docker: d,
config: config,
fs: util.NewFileSystem(),
tar: tar.New(),
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 will return true if the parameter provided is a valid, accessible directory that has contents (i.e. is not empty
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 produces by s2i
func (b *Layered) CreateDockerfile(config *api.Config) error {
buffer := bytes.Buffer{}
user, err := b.docker.GetImageUser(b.config.BuilderImage)
if err != nil {
return err
}
locations := []string{
filepath.Join(getDestination(config), "scripts"),
filepath.Join(getDestination(config), "src"),
}
buffer.WriteString(fmt.Sprintf("FROM %s\n", b.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(path.Join(config.WorkingDir, api.UploadScripts))
if scriptsIncluded {
glog.V(2).Infof("The scripts are included in %q directory", path.Join(config.WorkingDir, api.UploadScripts))
buffer.WriteString(fmt.Sprintf("COPY scripts %s\n", locations[0]))
} 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", path.Join(config.WorkingDir, api.UploadScripts))
}
buffer.WriteString(fmt.Sprintf("COPY src %s\n", locations[1]))
//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/9934
if len(user) > 0 {
buffer.WriteString("USER root\n")
if scriptsIncluded {
buffer.WriteString(fmt.Sprintf("RUN chown -R %s %s %s\n", user, locations[0], locations[1]))
} else {
buffer.WriteString(fmt.Sprintf("RUN chown -R %s %s\n", user, locations[1]))
}
buffer.WriteString(fmt.Sprintf("USER %s\n", user))
}
uploadDir := filepath.Join(b.config.WorkingDir, "upload")
if err := b.fs.WriteFile(filepath.Join(uploadDir, "Dockerfile"), buffer.Bytes()); err != nil {
return err
}
glog.V(2).Infof("Writing custom Dockerfile to %s", uploadDir)
return nil
}
// TODO: this should stop generating a file, and instead stream the tar.
//SourceTar returns a stream to the source tar file
func (b *Layered) SourceTar(config *api.Config) (io.ReadCloser, error) {
uploadDir := filepath.Join(config.WorkingDir, "upload")
tarFileName, err := b.tar.CreateTarFile(b.config.WorkingDir, uploadDir)
if err != nil {
return nil, err
}
return b.fs.Open(tarFileName)
}
//Build handles the `docker build` equivalent execution, returning the success/failure details
func (b *Layered) Build(config *api.Config) (*api.Result, error) {
if err := b.CreateDockerfile(config); err != nil {
return nil, err
}
glog.V(2).Info("Creating application source code image")
tarStream, err := b.SourceTar(config)
if err != nil {
return nil, err
}
defer tarStream.Close()
dockerImageReference, err := docker.ParseDockerImageReference(b.config.BuilderImage)
if err != nil {
return nil, err
}
// if we fall down this path via oc new-app, the builder image will be a docker image ref ending
// with a @<hex image id> instead of a tag; simply appending the time stamp to the end of a
// hex image id ref is not kosher with the docker API; so we remove the ID piece, and then
// construct the new image name
var newBuilderImage string
if len(dockerImageReference.ID) == 0 {
newBuilderImage = fmt.Sprintf("%s-%d", b.config.BuilderImage, time.Now().UnixNano())
} else {
if len(dockerImageReference.Registry) > 0 {
newBuilderImage = fmt.Sprintf("%s/", dockerImageReference.Registry)
}
if len(dockerImageReference.Namespace) > 0 {
newBuilderImage = fmt.Sprintf("%s%s/", newBuilderImage, dockerImageReference.Namespace)
}
newBuilderImage = fmt.Sprintf("%s%s:s2i-layered-%d", newBuilderImage, dockerImageReference.Name, time.Now().UnixNano())
}
outReader, outWriter := io.Pipe()
defer outReader.Close()
defer outWriter.Close()
opts := docker.BuildImageOptions{
Name: newBuilderImage,
Stdin: tarStream,
Stdout: outWriter,
CGroupLimits: config.CGroupLimits,
}
// goroutine to stream container's output
go func(reader io.Reader) {
scanner := bufio.NewReader(reader)
for {
text, err := scanner.ReadString('\n')
if err != nil {
// we're ignoring ErrClosedPipe, as this is information
// the docker container ended streaming logs
if glog.V(2) && err != io.ErrClosedPipe {
glog.Errorf("Error reading docker stdout, %v", err)
}
break
}
glog.V(2).Info(text)
}
}(outReader)
glog.V(2).Infof("Building new image %s with scripts and sources already inside", newBuilderImage)
if err = b.docker.BuildImage(opts); err != nil {
return nil, err
}
// upon successful build we need to modify current config
b.config.LayeredBuild = true
// new image name
b.config.BuilderImage = newBuilderImage
// see CreateDockerfile, conditional copy, location of scripts
scriptsIncluded := checkValidDirWithContents(path.Join(config.WorkingDir, api.UploadScripts))
glog.V(2).Infof("Scripts dir has contents %v", scriptsIncluded)
if scriptsIncluded {
b.config.ScriptsURL = "image://" + path.Join(getDestination(config), "scripts")
} else {
b.config.ScriptsURL, err = b.docker.GetScriptsURL(newBuilderImage)
if err != nil {
return nil, err
}
}
glog.V(2).Infof("Building %s using sti-enabled image", b.config.Tag)
if err := b.scripts.Execute(api.Assemble, config.AssembleUser, b.config); err != nil {
switch e := err.(type) {
case errors.ContainerError:
return nil, errors.NewAssembleError(b.config.Tag, e.Output, e)
default:
return nil, err
}
}
return &api.Result{
Success: true,
}, nil
}