File: build.go

package info (click to toggle)
singularity-container 4.0.3%2Bds1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 21,672 kB
  • sloc: asm: 3,857; sh: 2,125; ansic: 1,677; awk: 414; makefile: 110; python: 99
file content (219 lines) | stat: -rw-r--r-- 6,479 bytes parent folder | download | duplicates (2)
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
219
// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.

package client

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"runtime"

	jsonresp "github.com/sylabs/json-resp"
)

// rawBuildInfo contains the details of an individual build.
type rawBuildInfo struct {
	ID            string `json:"id"`
	IsComplete    bool   `json:"isComplete"`
	ImageSize     int64  `json:"imageSize,omitempty"`
	ImageChecksum string `json:"imageChecksum,omitempty"`
	LibraryRef    string `json:"libraryRef"`
	LibraryURL    string `json:"libraryURL"`
}

// BuildInfo contains the details of an individual build.
type BuildInfo struct {
	raw rawBuildInfo
}

func (bi *BuildInfo) ID() string            { return bi.raw.ID }
func (bi *BuildInfo) IsComplete() bool      { return bi.raw.IsComplete }
func (bi *BuildInfo) ImageSize() int64      { return bi.raw.ImageSize }
func (bi *BuildInfo) ImageChecksum() string { return bi.raw.ImageChecksum }
func (bi *BuildInfo) LibraryRef() string    { return bi.raw.LibraryRef }
func (bi *BuildInfo) LibraryURL() string    { return bi.raw.LibraryURL }

type buildOptions struct {
	libraryRef    string
	arch          string
	libraryURL    string
	contextDigest string
	workingDir    string
}

type BuildOption func(*buildOptions) error

// OptBuildLibraryRef sets the Library image ref to push to.
func OptBuildLibraryRef(imageRef string) BuildOption {
	return func(bo *buildOptions) error {
		bo.libraryRef = imageRef
		return nil
	}
}

// OptBuildArchitecture sets the build architecture to arch.
func OptBuildArchitecture(arch string) BuildOption {
	return func(bo *buildOptions) error {
		bo.arch = arch
		return nil
	}
}

// OptBuildLibraryPullBaseURL sets the base URL to pull images from when a build involves pulling
// one or more image(s) from a Library source.
func OptBuildLibraryPullBaseURL(libraryURL string) BuildOption {
	return func(bo *buildOptions) error {
		bo.libraryURL = libraryURL
		return nil
	}
}

// OptBuildContext instructs the Build Service to expose the build context with the specified
// digest during the build. The build context must be uploaded using UploadBuildContext.
func OptBuildContext(digest string) BuildOption {
	return func(bo *buildOptions) error {
		bo.contextDigest = digest
		return nil
	}
}

// OptBuildWorkingDirectory sets dir as the current working directory to include in the request.
func OptBuildWorkingDirectory(dir string) BuildOption {
	return func(bo *buildOptions) error {
		dir, err := filepath.Abs(dir)
		if err != nil {
			return err
		}

		bo.workingDir = dir
		return nil
	}
}

// Submit sends a build job to the Build Service. The context controls the lifetime of the request.
//
// By default, the built image will be pushed to an ephemeral location in the Library associated
// with the Remote Builder. To publish to a non-ephemeral location, consider using
// OptBuildLibraryRef.
//
// By default, the image will be built for the architecture returned by runtime.GOARCH. To override
// this behaviour, consider using OptBuildArchitecture.
//
// By default, if definition involves pulling one or more images from a Library reference that does
// not contain a hostname, they will be pulled from the Library associated with the Remote Builder.
// To override this behaviour, consider using OptBuildLibraryPullBaseURL.
//
// By default, local files referenced in the supplied definition will not be available on the Build
// Service. To expose local files, consider using OptBuildContext.
//
// The client includes the current working directory in the request, since the supplied definition
// may include paths that are relative to it. By default, the client attempts to derive the current
// working directory using os.Getwd(), falling back to "/" on error. To override this behaviour,
// consider using OptBuildWorkingDirectory.
func (c *Client) Submit(ctx context.Context, definition io.Reader, opts ...BuildOption) (*BuildInfo, error) {
	bo := buildOptions{
		arch:       runtime.GOARCH,
		workingDir: "/",
	}

	if dir, err := os.Getwd(); err == nil {
		bo.workingDir = dir
	}

	for _, opt := range opts {
		if err := opt(&bo); err != nil {
			return nil, fmt.Errorf("%w", err)
		}
	}

	raw, err := io.ReadAll(definition)
	if err != nil {
		return nil, fmt.Errorf("%w", err)
	}

	v := struct {
		DefinitionRaw       []byte            `json:"definitionRaw"`
		LibraryRef          string            `json:"libraryRef"`
		LibraryURL          string            `json:"libraryURL,omitempty"`
		BuilderRequirements map[string]string `json:"builderRequirements,omitempty"`
		ContextDigest       string            `json:"contextDigest,omitempty"`
		WorkingDir          string            `json:"workingDir,omitempty"`
	}{
		DefinitionRaw: raw,
		LibraryRef:    bo.libraryRef,
		LibraryURL:    bo.libraryURL,
		ContextDigest: bo.contextDigest,
		WorkingDir:    bo.workingDir,
	}

	if bo.arch != "" {
		v.BuilderRequirements = map[string]string{
			"arch": bo.arch,
		}
	}

	b, err := json.Marshal(v)
	if err != nil {
		return nil, fmt.Errorf("%w", err)
	}

	ref := &url.URL{
		Path: "v1/build",
	}

	req, err := c.newRequest(ctx, http.MethodPost, ref, bytes.NewReader(b))
	if err != nil {
		return nil, fmt.Errorf("%w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	res, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("%w", err)
	}
	defer res.Body.Close()

	if res.StatusCode/100 != 2 { // non-2xx status code
		return nil, fmt.Errorf("%w", errorFromResponse(res))
	}

	var rbi rawBuildInfo
	if err = jsonresp.ReadResponse(res.Body, &rbi); err != nil {
		return nil, fmt.Errorf("%w", err)
	}

	return &BuildInfo{rbi}, nil
}

// Cancel cancels an existing build. The context controls the lifetime of the request.
func (c *Client) Cancel(ctx context.Context, buildID string) error {
	ref := &url.URL{
		Path: fmt.Sprintf("v1/build/%v/_cancel", buildID),
	}

	req, err := c.newRequest(ctx, http.MethodPut, ref, nil)
	if err != nil {
		return fmt.Errorf("%w", err)
	}

	res, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("%w", err)
	}
	defer res.Body.Close()

	if res.StatusCode/100 != 2 { // non-2xx status code
		return fmt.Errorf("%w", errorFromResponse(res))
	}

	return nil
}