Skip to content

Commit 03149e0

Browse files
authored
Merge pull request #58 from factorysh/features/image-pull
Image pull
2 parents 8226634 + 03d9097 commit 03149e0

File tree

8 files changed

+171
-13
lines changed

8 files changed

+171
-13
lines changed

application/application.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,11 @@ func New(cfg *conf.Conf) (*Application, error) {
166166
r.Route("/{commit}", func(r chi.Router) {
167167
r.Group(func(r chi.Router) {
168168
r.Use(authMiddleware.Middleware())
169-
r.Post("/", a.PostTaskHandler) // create a new Task
170-
r.Get("/", a.TaskHandler(false)) // what is the state of this Task
171-
r.Get("/volumes/*", a.VolumesHandler(6, false)) // data wrote by docker run
172-
r.Get("/logs", a.TaskLogsHandler(false)) // stdout/stderr of the docker run
173-
r.Get("/sink", a.SinkHandler) // follow the task status
169+
r.Post("/", a.PostTaskHandler)
170+
r.Post("/_image", a.PostImageHandler)
171+
r.Get("/", a.TaskHandler(false))
172+
r.Get("/volumes/*", a.VolumesHandler(6, false))
173+
r.Get("/logs", a.TaskLogsHandler(false))
174174
})
175175
r.Group(func(r chi.Router) {
176176
r.Use(a.RefererMiddleware)

application/images.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package application
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"io/ioutil"
9+
"net/http"
10+
"net/url"
11+
"strings"
12+
"time"
13+
14+
"github.com/docker/docker/api/types"
15+
"github.com/docker/docker/client"
16+
"github.com/go-chi/chi/v5"
17+
"go.uber.org/zap"
18+
)
19+
20+
// ImageParams describe body data
21+
type ImageParams struct {
22+
Name string `json:"name"`
23+
AuthToken string `json:"auth_token"`
24+
}
25+
26+
// PostImageHandler is used to pull the request image, see https://docs.docker.com/engine/api/sdk/examples/#pull-an-image-with-authentication
27+
func (a *Application) PostImageHandler(w http.ResponseWriter, r *http.Request) {
28+
var imageParams ImageParams
29+
30+
serviceID := chi.URLParam(r, "serviceID")
31+
project := chi.URLParam(r, "project")
32+
commit := chi.URLParam(r, "commit")
33+
l := a.logger.With(
34+
zap.String("service", serviceID),
35+
zap.String("project", project),
36+
zap.String("commit", commit),
37+
)
38+
39+
if !a.Services[serviceID].Meta().UserDockerCompose {
40+
l.Warn("service does not accept custom image")
41+
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
42+
return
43+
}
44+
45+
d := json.NewDecoder(r.Body)
46+
err := d.Decode(&imageParams)
47+
if err != nil {
48+
l.Warn("decoding post image handler params error", zap.Error(err))
49+
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
50+
return
51+
}
52+
53+
if err := verifyImageName(imageParams.Name, project, commit); err != nil {
54+
l.Warn("verifying image name error", zap.Error(err))
55+
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
56+
return
57+
}
58+
59+
authConfig := types.AuthConfig{
60+
Username: "gitlab-ci-token",
61+
Password: imageParams.AuthToken,
62+
}
63+
encoded, err := json.Marshal(authConfig)
64+
if err != nil {
65+
l.Warn("encoding auth config", zap.Error(err))
66+
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
67+
return
68+
}
69+
70+
auth := base64.URLEncoding.EncodeToString(encoded)
71+
cli, err := client.NewClientWithOpts(client.FromEnv)
72+
if err != nil {
73+
l.Warn("new client error", zap.Error(err))
74+
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
75+
return
76+
}
77+
78+
ended := make(chan bool)
79+
f, flushable := w.(http.Flusher)
80+
if flushable {
81+
go func() {
82+
ticker := time.NewTicker(1 * time.Second)
83+
for {
84+
select {
85+
case <-ticker.C:
86+
io.WriteString(w, "#")
87+
if flushable {
88+
f.Flush()
89+
}
90+
case <-ended:
91+
return
92+
case <-r.Context().Done():
93+
return
94+
}
95+
}
96+
}()
97+
}
98+
99+
out, err := cli.ImagePull(r.Context(), imageParams.Name, types.ImagePullOptions{RegistryAuth: auth})
100+
if err != nil {
101+
l.Warn("error when downloading image", zap.Error(err))
102+
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
103+
return
104+
}
105+
defer out.Close()
106+
107+
// ⚠ consume the reader or nothing happens
108+
_, err = ioutil.ReadAll(out)
109+
if flushable {
110+
ended <- true
111+
}
112+
if err != nil {
113+
l.Warn("error when downloading image", zap.Error(err))
114+
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
115+
return
116+
}
117+
118+
w.Write([]byte("|> Image pulled successfully\n"))
119+
w.WriteHeader(http.StatusOK)
120+
}
121+
122+
func verifyImageName(name string, project string, commit string) error {
123+
parts := strings.Split(name, ":")
124+
125+
if len(parts) < 2 {
126+
return fmt.Errorf("invalid image name %s", name)
127+
}
128+
129+
if parts[len(parts)-1] != commit {
130+
return fmt.Errorf("image label name is not equal to commit sha : %s != %s", parts[len(parts)-1], commit)
131+
}
132+
133+
unescaped, err := url.PathUnescape(project)
134+
if err != nil {
135+
return err
136+
}
137+
138+
if !strings.Contains(name, unescaped) {
139+
return fmt.Errorf("image %s do not match project %s", name, unescaped)
140+
}
141+
142+
return nil
143+
}

application/task.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"strings"
1313
"time"
1414

15+
docker "github.com/docker/docker/client"
1516
"github.com/docker/docker/pkg/stdcopy"
1617
_claims "github.com/factorysh/microdensity/claims"
1718
"github.com/factorysh/microdensity/html"
@@ -231,6 +232,13 @@ func (a *Application) TaskLogzHandler(latest bool) func(http.ResponseWriter, *ht
231232
}
232233

233234
reader, err := t.Logs(r.Context(), false)
235+
if docker.IsErrNotFound(err) {
236+
l.Warn("container not found", zap.Error(err))
237+
w.WriteHeader(http.StatusNotFound)
238+
w.Write([]byte(http.StatusText(http.StatusNotFound)))
239+
return
240+
}
241+
234242
if err != nil {
235243
l.Warn("Task log error", zap.Error(err))
236244
w.WriteHeader(http.StatusInternalServerError)
@@ -280,6 +288,13 @@ func (a *Application) TaskLogsHandler(latest bool) func(http.ResponseWriter, *ht
280288
}
281289

282290
err = a.renderLogsPageForTask(r.Context(), t, w)
291+
if docker.IsErrNotFound(err) {
292+
l.Warn("container not found", zap.Error(err))
293+
w.WriteHeader(http.StatusNotFound)
294+
w.Write([]byte(http.StatusText(http.StatusNotFound)))
295+
return
296+
}
297+
283298
if err != nil {
284299
l.Warn("when rendering a logs page", zap.Error(err))
285300
w.WriteHeader(http.StatusInternalServerError)

run/compose.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func (c *ComposeRun) Cancel() {
4646
cancelFunc()
4747
}
4848

49-
func NewComposeRun(home string) (*ComposeRun, error) {
49+
func NewComposeRun(home string, env map[string]string) (*ComposeRun, error) {
5050
logger, err := zap.NewProduction()
5151
if err != nil {
5252
return nil, err
@@ -71,7 +71,7 @@ func NewComposeRun(home string) (*ComposeRun, error) {
7171
return nil, err
7272
}
7373

74-
project, details, err := LoadCompose(home)
74+
project, details, err := LoadCompose(home, env)
7575
if err != nil {
7676
l.Error("Load compose error", zap.Error(err))
7777
return nil, err
@@ -247,7 +247,7 @@ func (c *ComposeRun) runCommand(stdout io.WriteCloser, stderr io.WriteCloser, co
247247
}
248248

249249
// LoadCompose loads a docker-compose.yml file
250-
func LoadCompose(home string) (*types.Project, *types.ConfigDetails, error) {
250+
func LoadCompose(home string, env map[string]string) (*types.Project, *types.ConfigDetails, error) {
251251
path := filepath.Join(home, "docker-compose.yml")
252252
cfg, err := os.Open(path)
253253
if err != nil {
@@ -268,7 +268,7 @@ func LoadCompose(home string) (*types.Project, *types.ConfigDetails, error) {
268268
Content: raw,
269269
},
270270
},
271-
Environment: map[string]string{},
271+
Environment: env,
272272
}
273273

274274
p, err := loader.Load(details)

run/compose_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func TestCompose(t *testing.T) {
3434
t.Skip("Skipping testing in CI environment")
3535
}
3636

37-
cr, err := NewComposeRun("../demo/services/demo")
37+
cr, err := NewComposeRun("../demo/services/demo", map[string]string{})
3838
assert.NoError(t, err)
3939
buff := &bytes.Buffer{}
4040

run/run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (r *Runner) Prepare(t *task.Task, env map[string]string) (string, error) {
7070
return "", fmt.Errorf("task with id `%s` already prepared", t.Id)
7171
}
7272

73-
runnable, err := NewComposeRun(fmt.Sprintf("%s/%s", r.servicesDir, t.Service))
73+
runnable, err := NewComposeRun(fmt.Sprintf("%s/%s", r.servicesDir, t.Service), env)
7474
if err != nil {
7575
return "", err
7676
}

service/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ type Service interface {
2424
// Meta contains metadata about the linked service
2525
type Meta struct {
2626
Description string `yaml:"description"`
27-
UserDockerCompose bool `yaml:"bool"`
27+
UserDockerCompose bool `yaml:"user_docker_compose"`
2828
}

service/validation.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func ValidateServicesDefinitions(servicesDir string) error {
2828
}
2929

3030
func validateServiceDefinition(path string) error {
31-
p, _, err := run.LoadCompose(path)
31+
p, _, err := run.LoadCompose(path, map[string]string{})
3232
if err != nil {
3333
return err
3434
}

0 commit comments

Comments
 (0)