Imported Upstream version 0.7.1+dfsg
Dmitry Smirnov
8 years ago
1 | 1 | |
2 | 2 | language: go |
3 | 3 | |
4 | go: | |
5 | - 1.3 | |
6 | - 1.4 | |
7 | ||
8 | install: | |
9 | - go get golang.org/x/tools/cmd/cover | |
10 | - go get golang.org/x/tools/cmd/vet | |
4 | matrix: | |
5 | include: | |
6 | - go: 1.3.3 | |
7 | install: | |
8 | - go get golang.org/x/tools/cmd/cover | |
9 | - go get golang.org/x/tools/cmd/vet | |
10 | - go: 1.4.2 | |
11 | install: | |
12 | - go get golang.org/x/tools/cmd/cover | |
13 | - go get golang.org/x/tools/cmd/vet | |
14 | - go: 1.5.1 | |
11 | 15 | |
12 | 16 | script: |
13 | 17 | - ./test |
0 | ### v0.7.1 | |
1 | ||
2 | Minor release of the spec with one critical bug/consistency fix and a few tooling enhancements. | |
3 | ||
4 | 0.7.0 introduced a field to the app section of ImageManifests to allow users to specify supplementary group IDs. Unfortunately, this was implemented with inconsistent naming: `supplementaryGids` in the text of the spec itself, but `supplementaryGroups` in the schema code. | |
5 | ||
6 | In this release we standardise both the spec and the schema to `supplementaryGIDs`. See #516 for more information. | |
7 | ||
8 | Other changes in this release: | |
9 | - Added a callback to BuildWalker in the aci package to allow users to modify tar entries while building an ACI (#509) | |
10 | - Added an `--owner-root` flag to acbuild to adjust the uid/gid of all files in an ACI to 0:0 (#509) | |
11 | - Added a `--supplementary-gids` flag to actool's patch-manifest subcommand to adjust the supplementary group IDs of an ACI (#506, #516) | |
12 | - Added the ability to extract labels in the `lastditch` package (#508) | |
13 | - Changed the behaviour of the `NewAppFromString` parser in the `discovery` package to URL-encode label values before parsing them (#514) | |
14 | ||
15 | ### v0.7.0 | |
16 | ||
17 | Next major release of the spec, with a lot of tooling improvements, wording clarifications, and one breaking schema change from the previous release. | |
18 | ||
19 | Spec changes since v0.6.1: | |
20 | - The `mount` objects in pod manifests now refer directly to a path rather than referencing an app's mountPoint. This makes it easier for implementations to provide supplementary, arbitrary mounts in pods without needing to modify app sections in the pod manifest. This is a breaking schema change; however, implementations are suggested to preserve similar behaviour to the previous three-level mount mappings by continuing to use mountPoints name fields to generate mount<->volume mappings (#364, #495) | |
21 | - The resource/cpu unit has changed to cpu cores, instead of millicores (#468) | |
22 | - The wording around unpacking of ACI dependencies was reworked to clarify the expected order of unpacking (#425, #431, #494) | |
23 | - The wording around mounting volumes was expanded to advise much more explicit behaviour and protect various attack vectors for hosts (#431, #494) | |
24 | - App sections now support a `supplementaryGroups` field, which allows users to specify a list of additional GIDs that the processes of the app should run with (#339) | |
25 | - A new "Dependency Matching" section better explains the label matching process during dependency resolution (#469) | |
26 | - Clarified wording around the "empty" volume type (#449) | |
27 | ||
28 | Tooling changes and features: | |
29 | - Added an `IsValidOSArch` function implementations can use | |
30 | - When patching ACIs that do not have an `app` section in their manifest, actool will now automatically inject an App iff the user specifies an exec statement (#473, #489) | |
31 | - actool now warns if a manifest's ACVersion is too old (#322) | |
32 | - The NewCompressedReader function in the aci package now returns an io.ReadCloser instead of an io.Reader to facilitate closing the reader in the case of xz compression. (#462) | |
33 | - Added a new last-ditch parser of pod and image manifests to facilitate retrieving some debugging information for badly-formed manifests (#477) | |
34 | ||
35 | Schema and tooling bugfixes: | |
36 | - actool now validates layouts before opening output file so that it does not create empty files or truncate existing ones if a layout is invalid (#322) | |
37 | - Fixed a panic when `actool patch-manifest` is supplied with an ACI with no app section in its ImageManifest (#473) | |
38 | - ACE validator's name is ACName compatible (#448) | |
39 | - ACE validator's mountpoint checking is now more robust on Linux and works on FreeBSD (#467) | |
40 | - `build_aci` now works if NO_SIGNATURE is set | |
41 | - `build_aci` properly generates an armored signature (#460) | |
42 | - Fixed a typo in the ACE validator (uid should be uuid) (#485) | |
43 | ||
44 | Other changes: | |
45 | - Rewrote the kubernetes import path in Godeps (#471) | |
46 | - pkg/device is now buildable on OS X (#486) | |
47 | - schema code is now tested against Go 1.5 | |
48 | - Added explicit reference to RFC2119 regarding wording | |
49 | ||
0 | 50 | ### v0.6.1 |
1 | 51 | |
2 | 52 | Minor release of the spec; the most important change is adjusting the type for |
44 | 44 | |
45 | 45 | While the specification prescribes that the Meta Discovery process occurs over HTTP(S), it is intentionally agnostic with respect to the transport over which the discovered resource can be retrieved. |
46 | 46 | In the simplest cases (and as for example in Simple Discovery), the ACI payload can simply be retrieved over HTTPS itself. |
47 | However, in more advanced implementations - particulary in highly distributed environments - alternative protocols like [HDFS](hdfs) or [BitTorrent](bittorrent) could be used instead. | |
47 | However, in more advanced implementations - particularly in highly distributed environments - alternative protocols like [HDFS](hdfs) or [BitTorrent](bittorrent) could be used instead. | |
48 | 48 | |
49 | 49 | [hdfs]: http://hadoop.apache.org/docs/r1.2.1/hdfs_design.html |
50 | 50 | [bittorrent]: http://en.wikipedia.org/wiki/BitTorrent |
0 | { | |
1 | "ImportPath": "github.com/appc/spec", | |
2 | "GoVersion": "go1.4.1", | |
3 | "Packages": [ | |
4 | "./..." | |
5 | ], | |
6 | "Deps": [ | |
7 | { | |
8 | "ImportPath": "github.com/coreos/go-semver/semver", | |
9 | "Rev": "6fe83ccda8fb9b7549c9ab4ba47f47858bc950aa" | |
10 | }, | |
11 | { | |
12 | "ImportPath": "github.com/spf13/pflag", | |
13 | "Rev": "94e98a55fb412fcbcfc302555cb990f5e1590627" | |
14 | }, | |
15 | { | |
16 | "ImportPath": "golang.org/x/net/html", | |
17 | "Rev": "ccfcd82c7124abd517842acbacc3b8c1e390c73d" | |
18 | }, | |
19 | { | |
20 | "ImportPath": "k8s.io/kubernetes/pkg/api/resource", | |
21 | "Comment": "v1.0.2", | |
22 | "Rev": "e310e619fc1ac4f3238bf5ebe9e7033bf5d47ee2" | |
23 | }, | |
24 | { | |
25 | "ImportPath": "speter.net/go/exp/math/dec/inf", | |
26 | "Rev": "42ca6cd68aa922bc3f32f1e056e61b65945d9ad7" | |
27 | } | |
28 | ] | |
29 | } |
0 | This directory tree is generated automatically by godep. | |
1 | ||
2 | Please do not edit. | |
3 | ||
4 | See https://github.com/tools/godep for more information. |
0 | /* | |
1 | Copyright 2014 The Kubernetes Authors All rights reserved. | |
2 | ||
3 | Licensed under the Apache License, Version 2.0 (the "License"); | |
4 | you may not use this file except in compliance with the License. | |
5 | You may obtain a copy of the License at | |
6 | ||
7 | http://www.apache.org/licenses/LICENSE-2.0 | |
8 | ||
9 | Unless required by applicable law or agreed to in writing, software | |
10 | distributed under the License is distributed on an "AS IS" BASIS, | |
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 | See the License for the specific language governing permissions and | |
13 | limitations under the License. | |
14 | */ | |
15 | ||
16 | package resource | |
17 | ||
18 | import ( | |
19 | "errors" | |
20 | "fmt" | |
21 | "math/big" | |
22 | "regexp" | |
23 | "strings" | |
24 | ||
25 | flag "github.com/appc/spec/Godeps/_workspace/src/github.com/spf13/pflag" | |
26 | "github.com/appc/spec/Godeps/_workspace/src/speter.net/go/exp/math/dec/inf" | |
27 | ) | |
28 | ||
29 | // Quantity is a fixed-point representation of a number. | |
30 | // It provides convenient marshaling/unmarshaling in JSON and YAML, | |
31 | // in addition to String() and Int64() accessors. | |
32 | // | |
33 | // The serialization format is: | |
34 | // | |
35 | // <quantity> ::= <signedNumber><suffix> | |
36 | // (Note that <suffix> may be empty, from the "" case in <decimalSI>.) | |
37 | // <digit> ::= 0 | 1 | ... | 9 | |
38 | // <digits> ::= <digit> | <digit><digits> | |
39 | // <number> ::= <digits> | <digits>.<digits> | <digits>. | .<digits> | |
40 | // <sign> ::= "+" | "-" | |
41 | // <signedNumber> ::= <number> | <sign><number> | |
42 | // <suffix> ::= <binarySI> | <decimalExponent> | <decimalSI> | |
43 | // <binarySI> ::= Ki | Mi | Gi | Ti | Pi | Ei | |
44 | // (International System of units; See: http://physics.nist.gov/cuu/Units/binary.html) | |
45 | // <decimalSI> ::= m | "" | k | M | G | T | P | E | |
46 | // (Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.) | |
47 | // <decimalExponent> ::= "e" <signedNumber> | "E" <signedNumber> | |
48 | // | |
49 | // No matter which of the three exponent forms is used, no quantity may represent | |
50 | // a number greater than 2^63-1 in magnitude, nor may it have more than 3 decimal | |
51 | // places. Numbers larger or more precise will be capped or rounded up. | |
52 | // (E.g.: 0.1m will rounded up to 1m.) | |
53 | // This may be extended in the future if we require larger or smaller quantities. | |
54 | // | |
55 | // When a Quantity is parsed from a string, it will remember the type of suffix | |
56 | // it had, and will use the same type again when it is serialized. | |
57 | // | |
58 | // Before serializing, Quantity will be put in "canonical form". | |
59 | // This means that Exponent/suffix will be adjusted up or down (with a | |
60 | // corresponding increase or decrease in Mantissa) such that: | |
61 | // a. No precision is lost | |
62 | // b. No fractional digits will be emitted | |
63 | // c. The exponent (or suffix) is as large as possible. | |
64 | // The sign will be omitted unless the number is negative. | |
65 | // | |
66 | // Examples: | |
67 | // 1.5 will be serialized as "1500m" | |
68 | // 1.5Gi will be serialized as "1536Mi" | |
69 | // | |
70 | // NOTE: We reserve the right to amend this canonical format, perhaps to | |
71 | // allow 1.5 to be canonical. | |
72 | // TODO: Remove above disclaimer after all bikeshedding about format is over, | |
73 | // or after March 2015. | |
74 | // | |
75 | // Note that the quantity will NEVER be internally represented by a | |
76 | // floating point number. That is the whole point of this exercise. | |
77 | // | |
78 | // Non-canonical values will still parse as long as they are well formed, | |
79 | // but will be re-emitted in their canonical form. (So always use canonical | |
80 | // form, or don't diff.) | |
81 | // | |
82 | // This format is intended to make it difficult to use these numbers without | |
83 | // writing some sort of special handling code in the hopes that that will | |
84 | // cause implementors to also use a fixed point implementation. | |
85 | type Quantity struct { | |
86 | // Amount is public, so you can manipulate it if the accessor | |
87 | // functions are not sufficient. | |
88 | Amount *inf.Dec | |
89 | ||
90 | // Change Format at will. See the comment for Canonicalize for | |
91 | // more details. | |
92 | Format | |
93 | } | |
94 | ||
95 | // Format lists the three possible formattings of a quantity. | |
96 | type Format string | |
97 | ||
98 | const ( | |
99 | DecimalExponent = Format("DecimalExponent") // e.g., 12e6 | |
100 | BinarySI = Format("BinarySI") // e.g., 12Mi (12 * 2^20) | |
101 | DecimalSI = Format("DecimalSI") // e.g., 12M (12 * 10^6) | |
102 | ) | |
103 | ||
104 | // MustParse turns the given string into a quantity or panics; for tests | |
105 | // or others cases where you know the string is valid. | |
106 | func MustParse(str string) Quantity { | |
107 | q, err := ParseQuantity(str) | |
108 | if err != nil { | |
109 | panic(fmt.Errorf("cannot parse '%v': %v", str, err)) | |
110 | } | |
111 | return *q | |
112 | } | |
113 | ||
114 | const ( | |
115 | // splitREString is used to separate a number from its suffix; as such, | |
116 | // this is overly permissive, but that's OK-- it will be checked later. | |
117 | splitREString = "^([+-]?[0-9.]+)([eEimkKMGTP]*[-+]?[0-9]*)$" | |
118 | ) | |
119 | ||
120 | var ( | |
121 | // splitRE is used to get the various parts of a number. | |
122 | splitRE = regexp.MustCompile(splitREString) | |
123 | ||
124 | // Errors that could happen while parsing a string. | |
125 | ErrFormatWrong = errors.New("quantities must match the regular expression '" + splitREString + "'") | |
126 | ErrNumeric = errors.New("unable to parse numeric part of quantity") | |
127 | ErrSuffix = errors.New("unable to parse quantity's suffix") | |
128 | ||
129 | // Commonly needed big.Int values-- treat as read only! | |
130 | bigTen = big.NewInt(10) | |
131 | bigZero = big.NewInt(0) | |
132 | bigOne = big.NewInt(1) | |
133 | bigThousand = big.NewInt(1000) | |
134 | big1024 = big.NewInt(1024) | |
135 | ||
136 | // Commonly needed inf.Dec values-- treat as read only! | |
137 | decZero = inf.NewDec(0, 0) | |
138 | decOne = inf.NewDec(1, 0) | |
139 | decMinusOne = inf.NewDec(-1, 0) | |
140 | decThousand = inf.NewDec(1000, 0) | |
141 | dec1024 = inf.NewDec(1024, 0) | |
142 | decMinus1024 = inf.NewDec(-1024, 0) | |
143 | ||
144 | // Largest (in magnitude) number allowed. | |
145 | maxAllowed = inf.NewDec((1<<63)-1, 0) // == max int64 | |
146 | ||
147 | // The maximum value we can represent milli-units for. | |
148 | // Compare with the return value of Quantity.Value() to | |
149 | // see if it's safe to use Quantity.MilliValue(). | |
150 | MaxMilliValue = int64(((1 << 63) - 1) / 1000) | |
151 | ) | |
152 | ||
153 | // ParseQuantity turns str into a Quantity, or returns an error. | |
154 | func ParseQuantity(str string) (*Quantity, error) { | |
155 | parts := splitRE.FindStringSubmatch(strings.TrimSpace(str)) | |
156 | // regexp returns are entire match, followed by an entry for each () section. | |
157 | if len(parts) != 3 { | |
158 | return nil, ErrFormatWrong | |
159 | } | |
160 | ||
161 | amount := new(inf.Dec) | |
162 | if _, ok := amount.SetString(parts[1]); !ok { | |
163 | return nil, ErrNumeric | |
164 | } | |
165 | ||
166 | base, exponent, format, ok := quantitySuffixer.interpret(suffix(parts[2])) | |
167 | if !ok { | |
168 | return nil, ErrSuffix | |
169 | } | |
170 | ||
171 | // So that no one but us has to think about suffixes, remove it. | |
172 | if base == 10 { | |
173 | amount.SetScale(amount.Scale() + inf.Scale(-exponent)) | |
174 | } else if base == 2 { | |
175 | // numericSuffix = 2 ** exponent | |
176 | numericSuffix := big.NewInt(1).Lsh(bigOne, uint(exponent)) | |
177 | ub := amount.UnscaledBig() | |
178 | amount.SetUnscaledBig(ub.Mul(ub, numericSuffix)) | |
179 | } | |
180 | ||
181 | // Cap at min/max bounds. | |
182 | sign := amount.Sign() | |
183 | if sign == -1 { | |
184 | amount.Neg(amount) | |
185 | } | |
186 | // This rounds non-zero values up to the minimum representable | |
187 | // value, under the theory that if you want some resources, you | |
188 | // should get some resources, even if you asked for way too small | |
189 | // of an amount. | |
190 | // Arguably, this should be inf.RoundHalfUp (normal rounding), but | |
191 | // that would have the side effect of rounding values < .5m to zero. | |
192 | if v, ok := amount.Unscaled(); v != int64(0) || !ok { | |
193 | amount.Round(amount, 3, inf.RoundUp) | |
194 | } | |
195 | ||
196 | // The max is just a simple cap. | |
197 | if amount.Cmp(maxAllowed) > 0 { | |
198 | amount.Set(maxAllowed) | |
199 | } | |
200 | if format == BinarySI && amount.Cmp(decOne) < 0 && amount.Cmp(decZero) > 0 { | |
201 | // This avoids rounding and hopefully confusion, too. | |
202 | format = DecimalSI | |
203 | } | |
204 | if sign == -1 { | |
205 | amount.Neg(amount) | |
206 | } | |
207 | ||
208 | return &Quantity{amount, format}, nil | |
209 | } | |
210 | ||
211 | // removeFactors divides in a loop; the return values have the property that | |
212 | // d == result * factor ^ times | |
213 | // d may be modified in place. | |
214 | // If d == 0, then the return values will be (0, 0) | |
215 | func removeFactors(d, factor *big.Int) (result *big.Int, times int) { | |
216 | q := big.NewInt(0) | |
217 | m := big.NewInt(0) | |
218 | for d.Cmp(bigZero) != 0 { | |
219 | q.DivMod(d, factor, m) | |
220 | if m.Cmp(bigZero) != 0 { | |
221 | break | |
222 | } | |
223 | times++ | |
224 | d, q = q, d | |
225 | } | |
226 | return d, times | |
227 | } | |
228 | ||
229 | // Canonicalize returns the canonical form of q and its suffix (see comment on Quantity). | |
230 | // | |
231 | // Note about BinarySI: | |
232 | // * If q.Format is set to BinarySI and q.Amount represents a non-zero value between | |
233 | // -1 and +1, it will be emitted as if q.Format were DecimalSI. | |
234 | // * Otherwise, if q.Format is set to BinarySI, frational parts of q.Amount will be | |
235 | // rounded up. (1.1i becomes 2i.) | |
236 | func (q *Quantity) Canonicalize() (string, suffix) { | |
237 | if q.Amount == nil { | |
238 | return "0", "" | |
239 | } | |
240 | ||
241 | // zero is zero always | |
242 | if q.Amount.Cmp(&inf.Dec{}) == 0 { | |
243 | return "0", "" | |
244 | } | |
245 | ||
246 | format := q.Format | |
247 | switch format { | |
248 | case DecimalExponent, DecimalSI: | |
249 | case BinarySI: | |
250 | if q.Amount.Cmp(decMinus1024) > 0 && q.Amount.Cmp(dec1024) < 0 { | |
251 | // This avoids rounding and hopefully confusion, too. | |
252 | format = DecimalSI | |
253 | } else { | |
254 | tmp := &inf.Dec{} | |
255 | tmp.Round(q.Amount, 0, inf.RoundUp) | |
256 | if tmp.Cmp(q.Amount) != 0 { | |
257 | // Don't lose precision-- show as DecimalSI | |
258 | format = DecimalSI | |
259 | } | |
260 | } | |
261 | default: | |
262 | format = DecimalExponent | |
263 | } | |
264 | ||
265 | // TODO: If BinarySI formatting is requested but would cause rounding, upgrade to | |
266 | // one of the other formats. | |
267 | switch format { | |
268 | case DecimalExponent, DecimalSI: | |
269 | mantissa := q.Amount.UnscaledBig() | |
270 | exponent := int(-q.Amount.Scale()) | |
271 | amount := big.NewInt(0).Set(mantissa) | |
272 | // move all factors of 10 into the exponent for easy reasoning | |
273 | amount, times := removeFactors(amount, bigTen) | |
274 | exponent += times | |
275 | ||
276 | // make sure exponent is a multiple of 3 | |
277 | for exponent%3 != 0 { | |
278 | amount.Mul(amount, bigTen) | |
279 | exponent-- | |
280 | } | |
281 | ||
282 | suffix, _ := quantitySuffixer.construct(10, exponent, format) | |
283 | number := amount.String() | |
284 | return number, suffix | |
285 | case BinarySI: | |
286 | tmp := &inf.Dec{} | |
287 | tmp.Round(q.Amount, 0, inf.RoundUp) | |
288 | ||
289 | amount, exponent := removeFactors(tmp.UnscaledBig(), big1024) | |
290 | suffix, _ := quantitySuffixer.construct(2, exponent*10, format) | |
291 | number := amount.String() | |
292 | return number, suffix | |
293 | } | |
294 | return "0", "" | |
295 | } | |
296 | ||
297 | // String formats the Quantity as a string. | |
298 | func (q *Quantity) String() string { | |
299 | number, suffix := q.Canonicalize() | |
300 | return number + string(suffix) | |
301 | } | |
302 | ||
303 | // MarshalJSON implements the json.Marshaller interface. | |
304 | func (q Quantity) MarshalJSON() ([]byte, error) { | |
305 | return []byte(`"` + q.String() + `"`), nil | |
306 | } | |
307 | ||
308 | // UnmarshalJSON implements the json.Unmarshaller interface. | |
309 | func (q *Quantity) UnmarshalJSON(value []byte) error { | |
310 | str := string(value) | |
311 | parsed, err := ParseQuantity(strings.Trim(str, `"`)) | |
312 | if err != nil { | |
313 | return err | |
314 | } | |
315 | // This copy is safe because parsed will not be referred to again. | |
316 | *q = *parsed | |
317 | return nil | |
318 | } | |
319 | ||
320 | // NewQuantity returns a new Quantity representing the given | |
321 | // value in the given format. | |
322 | func NewQuantity(value int64, format Format) *Quantity { | |
323 | return &Quantity{ | |
324 | Amount: inf.NewDec(value, 0), | |
325 | Format: format, | |
326 | } | |
327 | } | |
328 | ||
329 | // NewMilliQuantity returns a new Quantity representing the given | |
330 | // value * 1/1000 in the given format. Note that BinarySI formatting | |
331 | // will round fractional values, and will be changed to DecimalSI for | |
332 | // values x where (-1 < x < 1) && (x != 0). | |
333 | func NewMilliQuantity(value int64, format Format) *Quantity { | |
334 | return &Quantity{ | |
335 | Amount: inf.NewDec(value, 3), | |
336 | Format: format, | |
337 | } | |
338 | } | |
339 | ||
340 | // Value returns the value of q; any fractional part will be lost. | |
341 | func (q *Quantity) Value() int64 { | |
342 | if q.Amount == nil { | |
343 | return 0 | |
344 | } | |
345 | tmp := &inf.Dec{} | |
346 | return tmp.Round(q.Amount, 0, inf.RoundUp).UnscaledBig().Int64() | |
347 | } | |
348 | ||
349 | // MilliValue returns the value of q * 1000; this could overflow an int64; | |
350 | // if that's a concern, call Value() first to verify the number is small enough. | |
351 | func (q *Quantity) MilliValue() int64 { | |
352 | if q.Amount == nil { | |
353 | return 0 | |
354 | } | |
355 | tmp := &inf.Dec{} | |
356 | return tmp.Round(tmp.Mul(q.Amount, decThousand), 0, inf.RoundUp).UnscaledBig().Int64() | |
357 | } | |
358 | ||
359 | // Set sets q's value to be value. | |
360 | func (q *Quantity) Set(value int64) { | |
361 | if q.Amount == nil { | |
362 | q.Amount = &inf.Dec{} | |
363 | } | |
364 | q.Amount.SetUnscaled(value) | |
365 | q.Amount.SetScale(0) | |
366 | } | |
367 | ||
368 | // SetMilli sets q's value to be value * 1/1000. | |
369 | func (q *Quantity) SetMilli(value int64) { | |
370 | if q.Amount == nil { | |
371 | q.Amount = &inf.Dec{} | |
372 | } | |
373 | q.Amount.SetUnscaled(value) | |
374 | q.Amount.SetScale(3) | |
375 | } | |
376 | ||
377 | // Copy is a convenience function that makes a deep copy for you. Non-deep | |
378 | // copies of quantities share pointers and you will regret that. | |
379 | func (q *Quantity) Copy() *Quantity { | |
380 | if q.Amount == nil { | |
381 | return NewQuantity(0, q.Format) | |
382 | } | |
383 | tmp := &inf.Dec{} | |
384 | return &Quantity{ | |
385 | Amount: tmp.Set(q.Amount), | |
386 | Format: q.Format, | |
387 | } | |
388 | } | |
389 | ||
390 | // qFlag is a helper type for the Flag function | |
391 | type qFlag struct { | |
392 | dest *Quantity | |
393 | } | |
394 | ||
395 | // Sets the value of the internal Quantity. (used by flag & pflag) | |
396 | func (qf qFlag) Set(val string) error { | |
397 | q, err := ParseQuantity(val) | |
398 | if err != nil { | |
399 | return err | |
400 | } | |
401 | // This copy is OK because q will not be referenced again. | |
402 | *qf.dest = *q | |
403 | return nil | |
404 | } | |
405 | ||
406 | // Converts the value of the internal Quantity to a string. (used by flag & pflag) | |
407 | func (qf qFlag) String() string { | |
408 | return qf.dest.String() | |
409 | } | |
410 | ||
411 | // States the type of flag this is (Quantity). (used by pflag) | |
412 | func (qf qFlag) Type() string { | |
413 | return "quantity" | |
414 | } | |
415 | ||
416 | // QuantityFlag is a helper that makes a quantity flag (using standard flag package). | |
417 | // Will panic if defaultValue is not a valid quantity. | |
418 | func QuantityFlag(flagName, defaultValue, description string) *Quantity { | |
419 | q := MustParse(defaultValue) | |
420 | flag.Var(NewQuantityFlagValue(&q), flagName, description) | |
421 | return &q | |
422 | } | |
423 | ||
424 | // NewQuantityFlagValue returns an object that can be used to back a flag, | |
425 | // pointing at the given Quantity variable. | |
426 | func NewQuantityFlagValue(q *Quantity) flag.Value { | |
427 | return qFlag{q} | |
428 | } |
0 | /* | |
1 | Copyright 2014 The Kubernetes Authors All rights reserved. | |
2 | ||
3 | Licensed under the Apache License, Version 2.0 (the "License"); | |
4 | you may not use this file except in compliance with the License. | |
5 | You may obtain a copy of the License at | |
6 | ||
7 | http://www.apache.org/licenses/LICENSE-2.0 | |
8 | ||
9 | Unless required by applicable law or agreed to in writing, software | |
10 | distributed under the License is distributed on an "AS IS" BASIS, | |
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 | See the License for the specific language governing permissions and | |
13 | limitations under the License. | |
14 | */ | |
15 | ||
16 | package resource_test | |
17 | ||
18 | import ( | |
19 | "fmt" | |
20 | ||
21 | "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" | |
22 | ) | |
23 | ||
24 | func ExampleFormat() { | |
25 | memorySize := resource.NewQuantity(5*1024*1024*1024, resource.BinarySI) | |
26 | fmt.Printf("memorySize = %v\n", memorySize) | |
27 | ||
28 | diskSize := resource.NewQuantity(5*1000*1000*1000, resource.DecimalSI) | |
29 | fmt.Printf("diskSize = %v\n", diskSize) | |
30 | ||
31 | cores := resource.NewMilliQuantity(5300, resource.DecimalSI) | |
32 | fmt.Printf("cores = %v\n", cores) | |
33 | ||
34 | // Output: | |
35 | // memorySize = 5Gi | |
36 | // diskSize = 5G | |
37 | // cores = 5300m | |
38 | } | |
39 | ||
40 | func ExampleMustParse() { | |
41 | memorySize := resource.MustParse("5Gi") | |
42 | fmt.Printf("memorySize = %v (%v)\n", memorySize.Value(), memorySize.Format) | |
43 | ||
44 | diskSize := resource.MustParse("5G") | |
45 | fmt.Printf("diskSize = %v (%v)\n", diskSize.Value(), diskSize.Format) | |
46 | ||
47 | cores := resource.MustParse("5300m") | |
48 | fmt.Printf("milliCores = %v (%v)\n", cores.MilliValue(), cores.Format) | |
49 | ||
50 | cores2 := resource.MustParse("5.4") | |
51 | fmt.Printf("milliCores = %v (%v)\n", cores2.MilliValue(), cores2.Format) | |
52 | ||
53 | // Output: | |
54 | // memorySize = 5368709120 (BinarySI) | |
55 | // diskSize = 5000000000 (DecimalSI) | |
56 | // milliCores = 5300 (DecimalSI) | |
57 | // milliCores = 5400 (DecimalSI) | |
58 | } |
0 | /* | |
1 | Copyright 2014 The Kubernetes Authors All rights reserved. | |
2 | ||
3 | Licensed under the Apache License, Version 2.0 (the "License"); | |
4 | you may not use this file except in compliance with the License. | |
5 | You may obtain a copy of the License at | |
6 | ||
7 | http://www.apache.org/licenses/LICENSE-2.0 | |
8 | ||
9 | Unless required by applicable law or agreed to in writing, software | |
10 | distributed under the License is distributed on an "AS IS" BASIS, | |
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 | See the License for the specific language governing permissions and | |
13 | limitations under the License. | |
14 | */ | |
15 | ||
16 | package resource | |
17 | ||
18 | import ( | |
19 | //"reflect" | |
20 | "encoding/json" | |
21 | "testing" | |
22 | ||
23 | "github.com/appc/spec/Godeps/_workspace/src/github.com/spf13/pflag" | |
24 | "github.com/appc/spec/Godeps/_workspace/src/speter.net/go/exp/math/dec/inf" | |
25 | fuzz "github.com/google/gofuzz" | |
26 | ) | |
27 | ||
28 | var ( | |
29 | testQuantityFlag = QuantityFlag("quantityFlag", "1M", "dummy flag for testing the quantity flag mechanism") | |
30 | ) | |
31 | ||
32 | func dec(i int64, exponent int) *inf.Dec { | |
33 | // See the below test-- scale is the negative of an exponent. | |
34 | return inf.NewDec(i, inf.Scale(-exponent)) | |
35 | } | |
36 | ||
37 | func TestDec(t *testing.T) { | |
38 | table := []struct { | |
39 | got *inf.Dec | |
40 | expect string | |
41 | }{ | |
42 | {dec(1, 0), "1"}, | |
43 | {dec(1, 1), "10"}, | |
44 | {dec(5, 2), "500"}, | |
45 | {dec(8, 3), "8000"}, | |
46 | {dec(2, 0), "2"}, | |
47 | {dec(1, -1), "0.1"}, | |
48 | {dec(3, -2), "0.03"}, | |
49 | {dec(4, -3), "0.004"}, | |
50 | } | |
51 | ||
52 | for _, item := range table { | |
53 | if e, a := item.expect, item.got.String(); e != a { | |
54 | t.Errorf("expected %v, got %v", e, a) | |
55 | } | |
56 | } | |
57 | } | |
58 | ||
59 | // TestQuantityParseZero ensures that when a 0 quantity is passed, its string value is 0 | |
60 | func TestQuantityParseZero(t *testing.T) { | |
61 | zero := MustParse("0") | |
62 | if expected, actual := "0", zero.String(); expected != actual { | |
63 | t.Errorf("Expected %v, actual %v", expected, actual) | |
64 | } | |
65 | } | |
66 | ||
67 | // Verifies that you get 0 as canonical value if internal value is 0, and not 0<suffix> | |
68 | func TestQuantityCanocicalizeZero(t *testing.T) { | |
69 | val := MustParse("1000m") | |
70 | x := val.Amount | |
71 | y := dec(1, 0) | |
72 | z := val.Amount.Sub(x, y) | |
73 | zero := Quantity{z, DecimalSI} | |
74 | if expected, actual := "0", zero.String(); expected != actual { | |
75 | t.Errorf("Expected %v, actual %v", expected, actual) | |
76 | } | |
77 | } | |
78 | ||
79 | func TestQuantityParse(t *testing.T) { | |
80 | table := []struct { | |
81 | input string | |
82 | expect Quantity | |
83 | }{ | |
84 | {"0", Quantity{dec(0, 0), DecimalSI}}, | |
85 | {"0m", Quantity{dec(0, 0), DecimalSI}}, | |
86 | {"0Ki", Quantity{dec(0, 0), BinarySI}}, | |
87 | {"0k", Quantity{dec(0, 0), DecimalSI}}, | |
88 | {"0Mi", Quantity{dec(0, 0), BinarySI}}, | |
89 | {"0M", Quantity{dec(0, 0), DecimalSI}}, | |
90 | {"0Gi", Quantity{dec(0, 0), BinarySI}}, | |
91 | {"0G", Quantity{dec(0, 0), DecimalSI}}, | |
92 | {"0Ti", Quantity{dec(0, 0), BinarySI}}, | |
93 | {"0T", Quantity{dec(0, 0), DecimalSI}}, | |
94 | ||
95 | // Binary suffixes | |
96 | {"1Ki", Quantity{dec(1024, 0), BinarySI}}, | |
97 | {"8Ki", Quantity{dec(8*1024, 0), BinarySI}}, | |
98 | {"7Mi", Quantity{dec(7*1024*1024, 0), BinarySI}}, | |
99 | {"6Gi", Quantity{dec(6*1024*1024*1024, 0), BinarySI}}, | |
100 | {"5Ti", Quantity{dec(5*1024*1024*1024*1024, 0), BinarySI}}, | |
101 | {"4Pi", Quantity{dec(4*1024*1024*1024*1024*1024, 0), BinarySI}}, | |
102 | {"3Ei", Quantity{dec(3*1024*1024*1024*1024*1024*1024, 0), BinarySI}}, | |
103 | ||
104 | {"10Ti", Quantity{dec(10*1024*1024*1024*1024, 0), BinarySI}}, | |
105 | {"100Ti", Quantity{dec(100*1024*1024*1024*1024, 0), BinarySI}}, | |
106 | ||
107 | // Decimal suffixes | |
108 | {"3m", Quantity{dec(3, -3), DecimalSI}}, | |
109 | {"9", Quantity{dec(9, 0), DecimalSI}}, | |
110 | {"8k", Quantity{dec(8, 3), DecimalSI}}, | |
111 | {"7M", Quantity{dec(7, 6), DecimalSI}}, | |
112 | {"6G", Quantity{dec(6, 9), DecimalSI}}, | |
113 | {"5T", Quantity{dec(5, 12), DecimalSI}}, | |
114 | {"40T", Quantity{dec(4, 13), DecimalSI}}, | |
115 | {"300T", Quantity{dec(3, 14), DecimalSI}}, | |
116 | {"2P", Quantity{dec(2, 15), DecimalSI}}, | |
117 | {"1E", Quantity{dec(1, 18), DecimalSI}}, | |
118 | ||
119 | // Decimal exponents | |
120 | {"1E-3", Quantity{dec(1, -3), DecimalExponent}}, | |
121 | {"1e3", Quantity{dec(1, 3), DecimalExponent}}, | |
122 | {"1E6", Quantity{dec(1, 6), DecimalExponent}}, | |
123 | {"1e9", Quantity{dec(1, 9), DecimalExponent}}, | |
124 | {"1E12", Quantity{dec(1, 12), DecimalExponent}}, | |
125 | {"1e15", Quantity{dec(1, 15), DecimalExponent}}, | |
126 | {"1E18", Quantity{dec(1, 18), DecimalExponent}}, | |
127 | ||
128 | // Nonstandard but still parsable | |
129 | {"1e14", Quantity{dec(1, 14), DecimalExponent}}, | |
130 | {"1e13", Quantity{dec(1, 13), DecimalExponent}}, | |
131 | {"1e3", Quantity{dec(1, 3), DecimalExponent}}, | |
132 | {"100.035k", Quantity{dec(100035, 0), DecimalSI}}, | |
133 | ||
134 | // Things that look like floating point | |
135 | {"0.001", Quantity{dec(1, -3), DecimalSI}}, | |
136 | {"0.0005k", Quantity{dec(5, -1), DecimalSI}}, | |
137 | {"0.005", Quantity{dec(5, -3), DecimalSI}}, | |
138 | {"0.05", Quantity{dec(5, -2), DecimalSI}}, | |
139 | {"0.5", Quantity{dec(5, -1), DecimalSI}}, | |
140 | {"0.00050k", Quantity{dec(5, -1), DecimalSI}}, | |
141 | {"0.00500", Quantity{dec(5, -3), DecimalSI}}, | |
142 | {"0.05000", Quantity{dec(5, -2), DecimalSI}}, | |
143 | {"0.50000", Quantity{dec(5, -1), DecimalSI}}, | |
144 | {"0.5e0", Quantity{dec(5, -1), DecimalExponent}}, | |
145 | {"0.5e-1", Quantity{dec(5, -2), DecimalExponent}}, | |
146 | {"0.5e-2", Quantity{dec(5, -3), DecimalExponent}}, | |
147 | {"0.5e0", Quantity{dec(5, -1), DecimalExponent}}, | |
148 | {"10.035M", Quantity{dec(10035, 3), DecimalSI}}, | |
149 | ||
150 | {"1.2e3", Quantity{dec(12, 2), DecimalExponent}}, | |
151 | {"1.3E+6", Quantity{dec(13, 5), DecimalExponent}}, | |
152 | {"1.40e9", Quantity{dec(14, 8), DecimalExponent}}, | |
153 | {"1.53E12", Quantity{dec(153, 10), DecimalExponent}}, | |
154 | {"1.6e15", Quantity{dec(16, 14), DecimalExponent}}, | |
155 | {"1.7E18", Quantity{dec(17, 17), DecimalExponent}}, | |
156 | ||
157 | {"9.01", Quantity{dec(901, -2), DecimalSI}}, | |
158 | {"8.1k", Quantity{dec(81, 2), DecimalSI}}, | |
159 | {"7.123456M", Quantity{dec(7123456, 0), DecimalSI}}, | |
160 | {"6.987654321G", Quantity{dec(6987654321, 0), DecimalSI}}, | |
161 | {"5.444T", Quantity{dec(5444, 9), DecimalSI}}, | |
162 | {"40.1T", Quantity{dec(401, 11), DecimalSI}}, | |
163 | {"300.2T", Quantity{dec(3002, 11), DecimalSI}}, | |
164 | {"2.5P", Quantity{dec(25, 14), DecimalSI}}, | |
165 | {"1.01E", Quantity{dec(101, 16), DecimalSI}}, | |
166 | ||
167 | // Things that saturate/round | |
168 | {"3.001m", Quantity{dec(4, -3), DecimalSI}}, | |
169 | {"1.1E-3", Quantity{dec(2, -3), DecimalExponent}}, | |
170 | {"0.0001", Quantity{dec(1, -3), DecimalSI}}, | |
171 | {"0.0005", Quantity{dec(1, -3), DecimalSI}}, | |
172 | {"0.00050", Quantity{dec(1, -3), DecimalSI}}, | |
173 | {"0.5e-3", Quantity{dec(1, -3), DecimalExponent}}, | |
174 | {"0.9m", Quantity{dec(1, -3), DecimalSI}}, | |
175 | {"0.12345", Quantity{dec(124, -3), DecimalSI}}, | |
176 | {"0.12354", Quantity{dec(124, -3), DecimalSI}}, | |
177 | {"9Ei", Quantity{maxAllowed, BinarySI}}, | |
178 | {"9223372036854775807Ki", Quantity{maxAllowed, BinarySI}}, | |
179 | {"12E", Quantity{maxAllowed, DecimalSI}}, | |
180 | ||
181 | // We'll accept fractional binary stuff, too. | |
182 | {"100.035Ki", Quantity{dec(10243584, -2), BinarySI}}, | |
183 | {"0.5Mi", Quantity{dec(.5*1024*1024, 0), BinarySI}}, | |
184 | {"0.05Gi", Quantity{dec(536870912, -1), BinarySI}}, | |
185 | {"0.025Ti", Quantity{dec(274877906944, -1), BinarySI}}, | |
186 | ||
187 | // Things written by trolls | |
188 | {"0.000001Ki", Quantity{dec(2, -3), DecimalSI}}, // rounds up, changes format | |
189 | {".001", Quantity{dec(1, -3), DecimalSI}}, | |
190 | {".0001k", Quantity{dec(100, -3), DecimalSI}}, | |
191 | {"1.", Quantity{dec(1, 0), DecimalSI}}, | |
192 | {"1.G", Quantity{dec(1, 9), DecimalSI}}, | |
193 | } | |
194 | ||
195 | for _, item := range table { | |
196 | got, err := ParseQuantity(item.input) | |
197 | if err != nil { | |
198 | t.Errorf("%v: unexpected error: %v", item.input, err) | |
199 | continue | |
200 | } | |
201 | if e, a := item.expect.Amount, got.Amount; e.Cmp(a) != 0 { | |
202 | t.Errorf("%v: expected %v, got %v", item.input, e, a) | |
203 | } | |
204 | if e, a := item.expect.Format, got.Format; e != a { | |
205 | t.Errorf("%v: expected %#v, got %#v", item.input, e, a) | |
206 | } | |
207 | } | |
208 | ||
209 | // Try the negative version of everything | |
210 | desired := &inf.Dec{} | |
211 | for _, item := range table { | |
212 | got, err := ParseQuantity("-" + item.input) | |
213 | if err != nil { | |
214 | t.Errorf("-%v: unexpected error: %v", item.input, err) | |
215 | continue | |
216 | } | |
217 | desired.Neg(item.expect.Amount) | |
218 | if e, a := desired, got.Amount; e.Cmp(a) != 0 { | |
219 | t.Errorf("%v: expected %v, got %v", item.input, e, a) | |
220 | } | |
221 | if e, a := item.expect.Format, got.Format; e != a { | |
222 | t.Errorf("%v: expected %#v, got %#v", item.input, e, a) | |
223 | } | |
224 | } | |
225 | ||
226 | // Try everything with an explicit + | |
227 | for _, item := range table { | |
228 | got, err := ParseQuantity("+" + item.input) | |
229 | if err != nil { | |
230 | t.Errorf("-%v: unexpected error: %v", item.input, err) | |
231 | continue | |
232 | } | |
233 | if e, a := item.expect.Amount, got.Amount; e.Cmp(a) != 0 { | |
234 | t.Errorf("%v: expected %v, got %v", item.input, e, a) | |
235 | } | |
236 | if e, a := item.expect.Format, got.Format; e != a { | |
237 | t.Errorf("%v: expected %#v, got %#v", item.input, e, a) | |
238 | } | |
239 | } | |
240 | ||
241 | invalid := []string{ | |
242 | "1.1.M", | |
243 | "1+1.0M", | |
244 | "0.1mi", | |
245 | "0.1am", | |
246 | "aoeu", | |
247 | ".5i", | |
248 | "1i", | |
249 | "-3.01i", | |
250 | } | |
251 | for _, item := range invalid { | |
252 | _, err := ParseQuantity(item) | |
253 | if err == nil { | |
254 | t.Errorf("%v parsed unexpectedly", item) | |
255 | } | |
256 | } | |
257 | } | |
258 | ||
259 | func TestQuantityString(t *testing.T) { | |
260 | table := []struct { | |
261 | in Quantity | |
262 | expect string | |
263 | }{ | |
264 | {Quantity{dec(1024*1024*1024, 0), BinarySI}, "1Gi"}, | |
265 | {Quantity{dec(300*1024*1024, 0), BinarySI}, "300Mi"}, | |
266 | {Quantity{dec(6*1024, 0), BinarySI}, "6Ki"}, | |
267 | {Quantity{dec(1001*1024*1024*1024, 0), BinarySI}, "1001Gi"}, | |
268 | {Quantity{dec(1024*1024*1024*1024, 0), BinarySI}, "1Ti"}, | |
269 | {Quantity{dec(5, 0), BinarySI}, "5"}, | |
270 | {Quantity{dec(500, -3), BinarySI}, "500m"}, | |
271 | {Quantity{dec(1, 9), DecimalSI}, "1G"}, | |
272 | {Quantity{dec(1000, 6), DecimalSI}, "1G"}, | |
273 | {Quantity{dec(1000000, 3), DecimalSI}, "1G"}, | |
274 | {Quantity{dec(1000000000, 0), DecimalSI}, "1G"}, | |
275 | {Quantity{dec(1, -3), DecimalSI}, "1m"}, | |
276 | {Quantity{dec(80, -3), DecimalSI}, "80m"}, | |
277 | {Quantity{dec(1080, -3), DecimalSI}, "1080m"}, | |
278 | {Quantity{dec(108, -2), DecimalSI}, "1080m"}, | |
279 | {Quantity{dec(10800, -4), DecimalSI}, "1080m"}, | |
280 | {Quantity{dec(300, 6), DecimalSI}, "300M"}, | |
281 | {Quantity{dec(1, 12), DecimalSI}, "1T"}, | |
282 | {Quantity{dec(1234567, 6), DecimalSI}, "1234567M"}, | |
283 | {Quantity{dec(1234567, -3), BinarySI}, "1234567m"}, | |
284 | {Quantity{dec(3, 3), DecimalSI}, "3k"}, | |
285 | {Quantity{dec(1025, 0), BinarySI}, "1025"}, | |
286 | {Quantity{dec(0, 0), DecimalSI}, "0"}, | |
287 | {Quantity{dec(0, 0), BinarySI}, "0"}, | |
288 | {Quantity{dec(1, 9), DecimalExponent}, "1e9"}, | |
289 | {Quantity{dec(1, -3), DecimalExponent}, "1e-3"}, | |
290 | {Quantity{dec(80, -3), DecimalExponent}, "80e-3"}, | |
291 | {Quantity{dec(300, 6), DecimalExponent}, "300e6"}, | |
292 | {Quantity{dec(1, 12), DecimalExponent}, "1e12"}, | |
293 | {Quantity{dec(1, 3), DecimalExponent}, "1e3"}, | |
294 | {Quantity{dec(3, 3), DecimalExponent}, "3e3"}, | |
295 | {Quantity{dec(3, 3), DecimalSI}, "3k"}, | |
296 | {Quantity{dec(0, 0), DecimalExponent}, "0"}, | |
297 | } | |
298 | for _, item := range table { | |
299 | got := item.in.String() | |
300 | if e, a := item.expect, got; e != a { | |
301 | t.Errorf("%#v: expected %v, got %v", item.in, e, a) | |
302 | } | |
303 | } | |
304 | desired := &inf.Dec{} // Avoid modifying the values in the table. | |
305 | for _, item := range table { | |
306 | if item.in.Amount.Cmp(decZero) == 0 { | |
307 | // Don't expect it to print "-0" ever | |
308 | continue | |
309 | } | |
310 | q := item.in | |
311 | q.Amount = desired.Neg(q.Amount) | |
312 | if e, a := "-"+item.expect, q.String(); e != a { | |
313 | t.Errorf("%#v: expected %v, got %v", item.in, e, a) | |
314 | } | |
315 | } | |
316 | } | |
317 | ||
318 | func TestQuantityParseEmit(t *testing.T) { | |
319 | table := []struct { | |
320 | in string | |
321 | expect string | |
322 | }{ | |
323 | {"1Ki", "1Ki"}, | |
324 | {"1Mi", "1Mi"}, | |
325 | {"1Gi", "1Gi"}, | |
326 | {"1024Mi", "1Gi"}, | |
327 | {"1000M", "1G"}, | |
328 | {".000001Ki", "2m"}, | |
329 | } | |
330 | ||
331 | for _, item := range table { | |
332 | q, err := ParseQuantity(item.in) | |
333 | if err != nil { | |
334 | t.Errorf("Couldn't parse %v", item.in) | |
335 | continue | |
336 | } | |
337 | if e, a := item.expect, q.String(); e != a { | |
338 | t.Errorf("%#v: expected %v, got %v", item.in, e, a) | |
339 | } | |
340 | } | |
341 | for _, item := range table { | |
342 | q, err := ParseQuantity("-" + item.in) | |
343 | if err != nil { | |
344 | t.Errorf("Couldn't parse %v", item.in) | |
345 | continue | |
346 | } | |
347 | if q.Amount.Cmp(decZero) == 0 { | |
348 | continue | |
349 | } | |
350 | if e, a := "-"+item.expect, q.String(); e != a { | |
351 | t.Errorf("%#v: expected %v, got %v", item.in, e, a) | |
352 | } | |
353 | } | |
354 | } | |
355 | ||
356 | var fuzzer = fuzz.New().Funcs( | |
357 | func(q *Quantity, c fuzz.Continue) { | |
358 | q.Amount = &inf.Dec{} | |
359 | if c.RandBool() { | |
360 | q.Format = BinarySI | |
361 | if c.RandBool() { | |
362 | q.Amount.SetScale(0) | |
363 | q.Amount.SetUnscaled(c.Int63()) | |
364 | return | |
365 | } | |
366 | // Be sure to test cases like 1Mi | |
367 | q.Amount.SetScale(0) | |
368 | q.Amount.SetUnscaled(c.Int63n(1024) << uint(10*c.Intn(5))) | |
369 | return | |
370 | } | |
371 | if c.RandBool() { | |
372 | q.Format = DecimalSI | |
373 | } else { | |
374 | q.Format = DecimalExponent | |
375 | } | |
376 | if c.RandBool() { | |
377 | q.Amount.SetScale(inf.Scale(c.Intn(4))) | |
378 | q.Amount.SetUnscaled(c.Int63()) | |
379 | return | |
380 | } | |
381 | // Be sure to test cases like 1M | |
382 | q.Amount.SetScale(inf.Scale(3 - c.Intn(15))) | |
383 | q.Amount.SetUnscaled(c.Int63n(1000)) | |
384 | }, | |
385 | ) | |
386 | ||
387 | func TestJSON(t *testing.T) { | |
388 | for i := 0; i < 500; i++ { | |
389 | q := &Quantity{} | |
390 | fuzzer.Fuzz(q) | |
391 | b, err := json.Marshal(q) | |
392 | if err != nil { | |
393 | t.Errorf("error encoding %v", q) | |
394 | } | |
395 | q2 := &Quantity{} | |
396 | err = json.Unmarshal(b, q2) | |
397 | if err != nil { | |
398 | t.Errorf("%v: error decoding %v", q, string(b)) | |
399 | } | |
400 | if q2.Amount.Cmp(q.Amount) != 0 { | |
401 | t.Errorf("Expected equal: %v, %v (json was '%v')", q, q2, string(b)) | |
402 | } | |
403 | } | |
404 | } | |
405 | ||
406 | func TestMilliNewSet(t *testing.T) { | |
407 | table := []struct { | |
408 | value int64 | |
409 | format Format | |
410 | expect string | |
411 | exact bool | |
412 | }{ | |
413 | {1, DecimalSI, "1m", true}, | |
414 | {1000, DecimalSI, "1", true}, | |
415 | {1234000, DecimalSI, "1234", true}, | |
416 | {1024, BinarySI, "1024m", false}, // Format changes | |
417 | {1000000, "invalidFormatDefaultsToExponent", "1e3", true}, | |
418 | {1024 * 1024, BinarySI, "1048576m", false}, // Format changes | |
419 | } | |
420 | ||
421 | for _, item := range table { | |
422 | q := NewMilliQuantity(item.value, item.format) | |
423 | if e, a := item.expect, q.String(); e != a { | |
424 | t.Errorf("Expected %v, got %v; %#v", e, a, q) | |
425 | } | |
426 | if !item.exact { | |
427 | continue | |
428 | } | |
429 | q2, err := ParseQuantity(q.String()) | |
430 | if err != nil { | |
431 | t.Errorf("Round trip failed on %v", q) | |
432 | } | |
433 | if e, a := item.value, q2.MilliValue(); e != a { | |
434 | t.Errorf("Expected %v, got %v", e, a) | |
435 | } | |
436 | } | |
437 | ||
438 | for _, item := range table { | |
439 | q := NewQuantity(0, item.format) | |
440 | q.SetMilli(item.value) | |
441 | if e, a := item.expect, q.String(); e != a { | |
442 | t.Errorf("Set: Expected %v, got %v; %#v", e, a, q) | |
443 | } | |
444 | } | |
445 | } | |
446 | ||
447 | func TestNewSet(t *testing.T) { | |
448 | table := []struct { | |
449 | value int64 | |
450 | format Format | |
451 | expect string | |
452 | }{ | |
453 | {1, DecimalSI, "1"}, | |
454 | {1000, DecimalSI, "1k"}, | |
455 | {1234000, DecimalSI, "1234k"}, | |
456 | {1024, BinarySI, "1Ki"}, | |
457 | {1000000, "invalidFormatDefaultsToExponent", "1e6"}, | |
458 | {1024 * 1024, BinarySI, "1Mi"}, | |
459 | } | |
460 | ||
461 | for _, item := range table { | |
462 | q := NewQuantity(item.value, item.format) | |
463 | if e, a := item.expect, q.String(); e != a { | |
464 | t.Errorf("Expected %v, got %v; %#v", e, a, q) | |
465 | } | |
466 | q2, err := ParseQuantity(q.String()) | |
467 | if err != nil { | |
468 | t.Errorf("Round trip failed on %v", q) | |
469 | } | |
470 | if e, a := item.value, q2.Value(); e != a { | |
471 | t.Errorf("Expected %v, got %v", e, a) | |
472 | } | |
473 | } | |
474 | ||
475 | for _, item := range table { | |
476 | q := NewQuantity(0, item.format) | |
477 | q.Set(item.value) | |
478 | if e, a := item.expect, q.String(); e != a { | |
479 | t.Errorf("Set: Expected %v, got %v; %#v", e, a, q) | |
480 | } | |
481 | } | |
482 | } | |
483 | ||
484 | func TestUninitializedNoCrash(t *testing.T) { | |
485 | var q Quantity | |
486 | ||
487 | q.Value() | |
488 | q.MilliValue() | |
489 | q.Copy() | |
490 | q.String() | |
491 | q.MarshalJSON() | |
492 | } | |
493 | ||
494 | func TestCopy(t *testing.T) { | |
495 | q := NewQuantity(5, DecimalSI) | |
496 | c := q.Copy() | |
497 | c.Set(6) | |
498 | if q.Value() == 6 { | |
499 | t.Errorf("Copy didn't") | |
500 | } | |
501 | } | |
502 | ||
503 | func TestQFlagSet(t *testing.T) { | |
504 | qf := qFlag{&Quantity{}} | |
505 | qf.Set("1Ki") | |
506 | if e, a := "1Ki", qf.String(); e != a { | |
507 | t.Errorf("Unexpected result %v != %v", e, a) | |
508 | } | |
509 | } | |
510 | ||
511 | func TestQFlagIsPFlag(t *testing.T) { | |
512 | var pfv pflag.Value = qFlag{} | |
513 | if e, a := "quantity", pfv.Type(); e != a { | |
514 | t.Errorf("Unexpected result %v != %v", e, a) | |
515 | } | |
516 | } |
0 | /* | |
1 | Copyright 2014 The Kubernetes Authors All rights reserved. | |
2 | ||
3 | Licensed under the Apache License, Version 2.0 (the "License"); | |
4 | you may not use this file except in compliance with the License. | |
5 | You may obtain a copy of the License at | |
6 | ||
7 | http://www.apache.org/licenses/LICENSE-2.0 | |
8 | ||
9 | Unless required by applicable law or agreed to in writing, software | |
10 | distributed under the License is distributed on an "AS IS" BASIS, | |
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 | See the License for the specific language governing permissions and | |
13 | limitations under the License. | |
14 | */ | |
15 | ||
16 | package resource | |
17 | ||
18 | import ( | |
19 | "strconv" | |
20 | ) | |
21 | ||
22 | type suffix string | |
23 | ||
24 | // suffixer can interpret and construct suffixes. | |
25 | type suffixer interface { | |
26 | interpret(suffix) (base, exponent int, fmt Format, ok bool) | |
27 | construct(base, exponent int, fmt Format) (s suffix, ok bool) | |
28 | } | |
29 | ||
30 | // quantitySuffixer handles suffixes for all three formats that quantity | |
31 | // can handle. | |
32 | var quantitySuffixer = newSuffixer() | |
33 | ||
34 | type bePair struct { | |
35 | base, exponent int | |
36 | } | |
37 | ||
38 | type listSuffixer struct { | |
39 | suffixToBE map[suffix]bePair | |
40 | beToSuffix map[bePair]suffix | |
41 | } | |
42 | ||
43 | func (ls *listSuffixer) addSuffix(s suffix, pair bePair) { | |
44 | if ls.suffixToBE == nil { | |
45 | ls.suffixToBE = map[suffix]bePair{} | |
46 | } | |
47 | if ls.beToSuffix == nil { | |
48 | ls.beToSuffix = map[bePair]suffix{} | |
49 | } | |
50 | ls.suffixToBE[s] = pair | |
51 | ls.beToSuffix[pair] = s | |
52 | } | |
53 | ||
54 | func (ls *listSuffixer) lookup(s suffix) (base, exponent int, ok bool) { | |
55 | pair, ok := ls.suffixToBE[s] | |
56 | if !ok { | |
57 | return 0, 0, false | |
58 | } | |
59 | return pair.base, pair.exponent, true | |
60 | } | |
61 | ||
62 | func (ls *listSuffixer) construct(base, exponent int) (s suffix, ok bool) { | |
63 | s, ok = ls.beToSuffix[bePair{base, exponent}] | |
64 | return | |
65 | } | |
66 | ||
67 | type suffixHandler struct { | |
68 | decSuffixes listSuffixer | |
69 | binSuffixes listSuffixer | |
70 | } | |
71 | ||
72 | func newSuffixer() suffixer { | |
73 | sh := &suffixHandler{} | |
74 | ||
75 | sh.binSuffixes.addSuffix("Ki", bePair{2, 10}) | |
76 | sh.binSuffixes.addSuffix("Mi", bePair{2, 20}) | |
77 | sh.binSuffixes.addSuffix("Gi", bePair{2, 30}) | |
78 | sh.binSuffixes.addSuffix("Ti", bePair{2, 40}) | |
79 | sh.binSuffixes.addSuffix("Pi", bePair{2, 50}) | |
80 | sh.binSuffixes.addSuffix("Ei", bePair{2, 60}) | |
81 | // Don't emit an error when trying to produce | |
82 | // a suffix for 2^0. | |
83 | sh.decSuffixes.addSuffix("", bePair{2, 0}) | |
84 | ||
85 | sh.decSuffixes.addSuffix("m", bePair{10, -3}) | |
86 | sh.decSuffixes.addSuffix("", bePair{10, 0}) | |
87 | sh.decSuffixes.addSuffix("k", bePair{10, 3}) | |
88 | sh.decSuffixes.addSuffix("M", bePair{10, 6}) | |
89 | sh.decSuffixes.addSuffix("G", bePair{10, 9}) | |
90 | sh.decSuffixes.addSuffix("T", bePair{10, 12}) | |
91 | sh.decSuffixes.addSuffix("P", bePair{10, 15}) | |
92 | sh.decSuffixes.addSuffix("E", bePair{10, 18}) | |
93 | ||
94 | return sh | |
95 | } | |
96 | ||
97 | func (sh *suffixHandler) construct(base, exponent int, fmt Format) (s suffix, ok bool) { | |
98 | switch fmt { | |
99 | case DecimalSI: | |
100 | return sh.decSuffixes.construct(base, exponent) | |
101 | case BinarySI: | |
102 | return sh.binSuffixes.construct(base, exponent) | |
103 | case DecimalExponent: | |
104 | if base != 10 { | |
105 | return "", false | |
106 | } | |
107 | if exponent == 0 { | |
108 | return "", true | |
109 | } | |
110 | return suffix("e" + strconv.FormatInt(int64(exponent), 10)), true | |
111 | } | |
112 | return "", false | |
113 | } | |
114 | ||
115 | func (sh *suffixHandler) interpret(suffix suffix) (base, exponent int, fmt Format, ok bool) { | |
116 | // Try lookup tables first | |
117 | if b, e, ok := sh.decSuffixes.lookup(suffix); ok { | |
118 | return b, e, DecimalSI, true | |
119 | } | |
120 | if b, e, ok := sh.binSuffixes.lookup(suffix); ok { | |
121 | return b, e, BinarySI, true | |
122 | } | |
123 | ||
124 | if len(suffix) > 1 && (suffix[0] == 'E' || suffix[0] == 'e') { | |
125 | parsed, err := strconv.ParseInt(string(suffix[1:]), 10, 64) | |
126 | if err != nil { | |
127 | return 0, 0, DecimalExponent, false | |
128 | } | |
129 | return 10, int(parsed), DecimalExponent, true | |
130 | } | |
131 | ||
132 | return 0, 0, DecimalExponent, false | |
133 | } |
0 | 0 | # App Container |
1 | 1 | |
2 | 2 | [![Build Status](https://travis-ci.org/appc/spec.png?branch=master)](https://travis-ci.org/appc/spec) |
3 | ||
4 | ![appc logo](logos/appc-horizontal-color.png) | |
3 | 5 | |
4 | 6 | This repository contains schema definitions and tools for the App Container (appc) specification. |
5 | 7 | These include technical details on how an appc image is downloaded over a network, cryptographically verified, and executed on a host. |
37 | 39 | |
38 | 40 | - [goaci](https://github.com/appc/goaci) - ACI builder for Go projects |
39 | 41 | - [docker2aci](https://github.com/appc/docker2aci) - ACI builder from Docker images |
40 | - [actool](https://github.com/appc/spec/tree/master/actool) - ACI builder from root filesystems | |
42 | - [deb2aci](https://github.com/klizhentas/deb2aci) - ACI builder from Debian packages | |
43 | - [actool](https://github.com/appc/spec/tree/master/actool) - Simple tool to assemble ACIs from root filesystems | |
44 | - [acbuild](https://github.com/appc/acbuild) - A versatile tool for building and manipulating ACIs | |
41 | 45 | - [baci](https://github.com/sgotti/baci) - A generic ACI build project |
46 | - [openwrt-aci](https://github.com/1player/openwrt-aci) - A tool to build ACIs based on OpenWRT snapshots | |
47 | - [oci2aci](https://github.com/huawei-openlab/oci2aci) - ACI builder from OCI bundle | |
42 | 48 | |
43 | 49 | ## What are some implementations of the spec? |
44 | 50 | |
59 | 65 | |
60 | 66 | ### Building ACIs |
61 | 67 | |
62 | `actool` can be used to build an Application Container Image from an [Image Layout](SPEC.md#image-layout) - that is, from an Image Manifest and an application root filesystem (rootfs). | |
68 | Various tools [listed above](#what-is-the-promise-of-the-app-container-spec) can be used to build ACIs from existing images or based on other sources. | |
69 | ||
70 | As an example of building an ACI from scratch, `actool` can be used to build an Application Container Image from an [Image Layout](spec/aci.md#image-layout) - that is, from an Image Manifest and an application root filesystem (rootfs). | |
63 | 71 | |
64 | 72 | For example, to build a simple ACI (in this case consisting of a single binary), one could do the following: |
65 | 73 | ``` |
72 | 80 | $ cat /tmp/my-app/manifest |
73 | 81 | { |
74 | 82 | "acKind": "ImageManifest", |
75 | "acVersion": "0.6.1", | |
83 | "acVersion": "0.7.1", | |
76 | 84 | "name": "my-app", |
77 | 85 | "labels": [ |
78 | 86 | {"name": "os", "value": "linux"}, |
104 | 112 | $ tar xf /tmp/my-app.aci manifest -O | python -m json.tool |
105 | 113 | { |
106 | 114 | "acKind": "ImageManifest", |
107 | "acVersion": "0.6.1", | |
115 | "acVersion": "0.7.1", | |
108 | 116 | "annotations": null, |
109 | 117 | "app": { |
110 | 118 | "environment": [], |
13 | 13 | * Designing for composability and independent implementations |
14 | 14 | * Using common technologies for cryptography, archiving, compression and transport |
15 | 15 | * Using the DNS namespace to name and discover images |
16 | ||
17 | ### Requirements | |
18 | ||
19 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this and other documents of the specification are to be interpreted as described in [RFC2119](http://tools.ietf.org/html/rfc2119). | |
16 | 20 | |
17 | 21 | ## Sections |
18 | 22 |
4 | 4 | set -eu |
5 | 5 | |
6 | 6 | PREFIX="ace" |
7 | : ${NO_SIGNATURE=} | |
7 | 8 | |
8 | 9 | if ! [[ $0 =~ "${PREFIX}/build_aci" ]]; then |
9 | 10 | echo "invoke from repository root" 1>&2 |
30 | 31 | # TODO(jonboulle): create uncompressed instead, then gzip? |
31 | 32 | HASH=sha512-$(gzip -d -f ../ace-validator-${typ}.aci -c | openssl dgst -sha512 -hex -r | awk '{print $1}') |
32 | 33 | if [ -z "$NO_SIGNATURE" ] ; then |
33 | gpg --cipher-algo AES256 --output ace-validator-${typ}.aci.asc --detach-sig ../ace-validator-${typ}.aci | |
34 | gpg --cipher-algo AES256 --armor --output ace-validator-${typ}.aci.asc --detach-sig ../ace-validator-${typ}.aci | |
34 | 35 | mv ace-validator-${typ}.aci.asc ../ |
35 | 36 | fi |
36 | 37 | popd >/dev/null |
0 | 0 | { |
1 | "acVersion": "0.6.1", | |
1 | "acVersion": "0.7.1", | |
2 | 2 | "acKind": "ImageManifest", |
3 | 3 | "name": "coreos.com/ace-validator-main", |
4 | 4 | "labels": [ |
5 | { "name": "version", "value": "0.6.1" }, | |
5 | { "name": "version", "value": "0.7.1" }, | |
6 | 6 | { "name": "os", "value": "linux" }, |
7 | 7 | { "name": "arch", "value": "amd64" } |
8 | 8 | ], |
0 | 0 | { |
1 | "acVersion": "0.6.1", | |
1 | "acVersion": "0.7.1", | |
2 | 2 | "acKind": "ImageManifest", |
3 | 3 | "name": "coreos.com/ace-validator-sidekick", |
4 | 4 | "labels": [ |
5 | { "name": "version", "value": "0.6.1" }, | |
5 | { "name": "version", "value": "0.7.1" }, | |
6 | 6 | { "name": "os", "value": "linux" }, |
7 | 7 | { "name": "arch", "value": "amd64" } |
8 | 8 | ], |
0 | // Copyright 2015 The appc Authors | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | // you may not use this file except in compliance with the License. | |
4 | // You may obtain a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, | |
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | // See the License for the specific language governing permissions and | |
12 | // limitations under the License. | |
13 | ||
14 | // +build !linux | |
15 | // +build !freebsd | |
16 | ||
17 | package main | |
18 | ||
19 | import ( | |
20 | "syscall" | |
21 | ) | |
22 | ||
23 | func isSameFilesystem(a, b *syscall.Statfs_t) bool { | |
24 | return a.Fsid == b.Fsid | |
25 | } |
0 | // Copyright 2015 The appc Authors | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | // you may not use this file except in compliance with the License. | |
4 | // You may obtain a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, | |
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | // See the License for the specific language governing permissions and | |
12 | // limitations under the License. | |
13 | ||
14 | // +build freebsd | |
15 | ||
16 | package main | |
17 | ||
18 | import ( | |
19 | "syscall" | |
20 | ) | |
21 | ||
22 | func isSameFilesystem(a, b *syscall.Statfs_t) bool { | |
23 | if a.Fsid != (syscall.Fsid{}) || b.Fsid != (syscall.Fsid{}) { | |
24 | // If Fsid is not empty, we can just compare the IDs | |
25 | return a.Fsid == b.Fsid | |
26 | } | |
27 | // Fsids are zero, this happens in jails, but we can compare the rest | |
28 | return a.Fstypename == b.Fstypename && | |
29 | a.Mntfromname == b.Mntfromname && | |
30 | a.Mntonname == b.Mntonname | |
31 | } |
0 | // Copyright 2015 The appc Authors | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | // you may not use this file except in compliance with the License. | |
4 | // You may obtain a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, | |
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | // See the License for the specific language governing permissions and | |
12 | // limitations under the License. | |
13 | ||
14 | // +build linux | |
15 | ||
16 | package main | |
17 | ||
18 | import ( | |
19 | "fmt" | |
20 | "os" | |
21 | ) | |
22 | ||
23 | func checkMountImpl(d string, readonly bool) error { | |
24 | mountinfoPath := fmt.Sprintf("/proc/self/mountinfo") | |
25 | mi, err := os.Open(mountinfoPath) | |
26 | if err != nil { | |
27 | return err | |
28 | } | |
29 | defer mi.Close() | |
30 | ||
31 | isMounted, ro, err := parseMountinfo(mi, d) | |
32 | if err != nil { | |
33 | return err | |
34 | } | |
35 | if !isMounted { | |
36 | return fmt.Errorf("%q is not a mount point", d) | |
37 | } | |
38 | ||
39 | if ro == readonly { | |
40 | return nil | |
41 | } else { | |
42 | return fmt.Errorf("%q mounted ro=%t, want %t", d, ro, readonly) | |
43 | } | |
44 | } |
0 | // +build !linux | |
1 | ||
2 | package main | |
3 | ||
4 | import ( | |
5 | "fmt" | |
6 | "path/filepath" | |
7 | "syscall" | |
8 | ) | |
9 | ||
10 | func checkMountStatfs(d string, readonly bool) error { | |
11 | // or.... | |
12 | // os.Stat(path).Sys().(*syscall.Stat_t).Dev | |
13 | sfs1 := &syscall.Statfs_t{} | |
14 | if err := syscall.Statfs(d, sfs1); err != nil { | |
15 | return fmt.Errorf("error calling statfs on %q: %v", d, err) | |
16 | } | |
17 | sfs2 := &syscall.Statfs_t{} | |
18 | if err := syscall.Statfs(filepath.Dir(d), sfs2); err != nil { | |
19 | return fmt.Errorf("error calling statfs on %q: %v", d, err) | |
20 | } | |
21 | if isSameFilesystem(sfs1, sfs2) { | |
22 | return fmt.Errorf("%q is not a mount point", d) | |
23 | } | |
24 | ro := sfs1.Flags&syscall.O_RDONLY == 1 | |
25 | if ro != readonly { | |
26 | return fmt.Errorf("%q mounted ro=%t, want %t", d, ro, readonly) | |
27 | } | |
28 | ||
29 | return nil | |
30 | } | |
31 | ||
32 | func checkMountImpl(d string, readonly bool) error { | |
33 | return checkMountStatfs(d, readonly) | |
34 | } |
41 | 41 | */ |
42 | 42 | |
43 | 43 | import ( |
44 | "bufio" | |
44 | 45 | "bytes" |
45 | 46 | "encoding/json" |
46 | 47 | "fmt" |
48 | "io" | |
47 | 49 | "io/ioutil" |
48 | 50 | "net/http" |
49 | 51 | "net/url" |
50 | 52 | "os" |
51 | "path/filepath" | |
52 | 53 | "reflect" |
53 | 54 | "strings" |
54 | "syscall" | |
55 | 55 | "time" |
56 | 56 | |
57 | 57 | "github.com/appc/spec/schema" |
93 | 93 | }, |
94 | 94 | } |
95 | 95 | // "Name" |
96 | an = "coreos.com/ace-validator-main" | |
96 | an = "ace-validator-main" | |
97 | 97 | ) |
98 | 98 | |
99 | 99 | type results []error |
428 | 428 | // Verify |
429 | 429 | _, err = metadataPostForm(metadataURL, "/pod/hmac/verify", url.Values{ |
430 | 430 | "content": []string{plaintext}, |
431 | "uid": []string{string(uuid)}, | |
431 | "uuid": []string{string(uuid)}, | |
432 | 432 | "signature": []string{string(sig)}, |
433 | 433 | }) |
434 | 434 | |
470 | 470 | // checkMount checks that the given string is a mount point, and that it is |
471 | 471 | // mounted appropriately read-only or not according to the given bool |
472 | 472 | func checkMount(d string, readonly bool) error { |
473 | // or.... | |
474 | // os.Stat(path).Sys().(*syscall.Stat_t).Dev | |
475 | sfs1 := &syscall.Statfs_t{} | |
476 | if err := syscall.Statfs(d, sfs1); err != nil { | |
477 | return fmt.Errorf("error calling statfs on %q: %v", d, err) | |
478 | } | |
479 | sfs2 := &syscall.Statfs_t{} | |
480 | if err := syscall.Statfs(filepath.Dir(d), sfs2); err != nil { | |
481 | return fmt.Errorf("error calling statfs on %q: %v", d, err) | |
482 | } | |
483 | if sfs1.Fsid == sfs2.Fsid { | |
484 | return fmt.Errorf("%q is not a mount point", d) | |
485 | } | |
486 | ro := sfs1.Flags&syscall.O_RDONLY == 1 | |
487 | if ro != readonly { | |
488 | return fmt.Errorf("%q mounted ro=%t, want %t", d, ro, readonly) | |
489 | } | |
490 | return nil | |
473 | return checkMountImpl(d, readonly) | |
474 | } | |
475 | ||
476 | // parseMountinfo parses a Reader representing a /proc/PID/mountinfo file and | |
477 | // returns whether dir is mounted and if so, whether it is read-only or not | |
478 | func parseMountinfo(mountinfo io.Reader, dir string) (isMounted bool, readOnly bool, err error) { | |
479 | sc := bufio.NewScanner(mountinfo) | |
480 | for sc.Scan() { | |
481 | var ( | |
482 | mountID int | |
483 | parentID int | |
484 | majorMinor string | |
485 | root string | |
486 | mountPoint string | |
487 | mountOptions string | |
488 | ) | |
489 | ||
490 | _, err := fmt.Sscanf(sc.Text(), "%d %d %s %s %s %s", | |
491 | &mountID, &parentID, &majorMinor, &root, &mountPoint, &mountOptions) | |
492 | if err != nil { | |
493 | return false, false, err | |
494 | } | |
495 | ||
496 | if mountPoint == dir { | |
497 | isMounted = true | |
498 | optionsParts := strings.Split(mountOptions, ",") | |
499 | for _, o := range optionsParts { | |
500 | switch o { | |
501 | case "ro": | |
502 | readOnly = true | |
503 | case "rw": | |
504 | readOnly = false | |
505 | } | |
506 | } | |
507 | } | |
508 | } | |
509 | ||
510 | return | |
491 | 511 | } |
492 | 512 | |
493 | 513 | // assertNotExistsAndCreate asserts that a file at the given path does not |
0 | // Copyright 2015 The appc Authors | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | // you may not use this file except in compliance with the License. | |
4 | // You may obtain a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, | |
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | // See the License for the specific language governing permissions and | |
12 | // limitations under the License. | |
13 | ||
14 | package main | |
15 | ||
16 | import ( | |
17 | "strings" | |
18 | "testing" | |
19 | ) | |
20 | ||
21 | var mountinfoTests = []struct { | |
22 | mountinfo string | |
23 | mountPoint string | |
24 | readOnly bool | |
25 | expectMounted bool | |
26 | expectErr bool | |
27 | }{ | |
28 | { | |
29 | `15 19 0:4 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw | |
30 | 16 19 0:15 / /sys rw,nosuid,nodev,noexec,relatime shared:6 - sysfs sys rw | |
31 | 17 19 0:6 / /dev rw,nosuid,relatime shared:2 - devtmpfs dev rw,size=6089152k,nr_inodes=1522288,mode=755 | |
32 | 18 19 0:16 / /run rw,nosuid,nodev,relatime shared:12 - tmpfs run rw,mode=755 | |
33 | 19 0 254:0 / / rw,relatime shared:1 - ext4 /dev/mapper/cryptroot rw,discard,data=ordered | |
34 | 20 16 0:17 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:7 - securityfs securityfs rw | |
35 | 21 17 0:18 / /dev/shm rw,nosuid,nodev shared:3 - tmpfs tmpfs rw | |
36 | 66 19 8:2 / /mountPoint1 rw,relatime,nodev shared:26 slave:88 - ext4 /dev/sda2 rw,discard,data=ordered`, | |
37 | "/mountPoint1", | |
38 | false, | |
39 | true, | |
40 | false, | |
41 | }, | |
42 | { | |
43 | `15 19 0:4 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw | |
44 | 16 19 0:15 / /sys rw,nosuid,nodev,noexec,relatime shared:6 - sysfs sys rw | |
45 | 17 19 0:6 / /dev rw,nosuid,relatime shared:2 - devtmpfs dev rw,size=6089152k,nr_inodes=1522288,mode=755 | |
46 | 18 19 0:16 / /run rw,nosuid,nodev,relatime shared:12 - tmpfs run rw,mode=755 | |
47 | 66 19 8:2 / /mountPoint2 relatime,ro,nodev shared:26 slave:88 - ext4 /dev/sda2 rw,discard,data=ordered | |
48 | 19 0 254:0 / / rw,relatime shared:1 - ext4 /dev/mapper/cryptroot rw,discard,data=ordered | |
49 | 20 16 0:17 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:7 - securityfs securityfs rw | |
50 | 21 17 0:18 / /dev/shm rw,nosuid,nodev shared:3 - tmpfs tmpfs rw`, | |
51 | "/mountPoint2", | |
52 | true, | |
53 | true, | |
54 | false, | |
55 | }, | |
56 | { | |
57 | `15 19 0:4 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw | |
58 | 16 19 0:15 / /sys rw,nosuid,nodev,noexec,relatime shared:6 - sysfs sys rw | |
59 | 17 19 0:6 / /dev rw,nosuid,relatime shared:2 - devtmpfs dev rw,size=6089152k,nr_inodes=1522288,mode=755 | |
60 | 18 19 0:16 / /run rw,nosuid,nodev,relatime shared:12 - tmpfs run rw,mode=755 | |
61 | 19 0 254:0 / / rw,relatime shared:1 - ext4 /dev/mapper/cryptroot rw,discard,data=ordered | |
62 | 20 16 0:17 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:7 - securityfs securityfs rw | |
63 | 21 17 0:18 / /dev/shm rw,nosuid,nodev shared:3 - tmpfs tmpfs rw`, | |
64 | "/mountPoint3", | |
65 | false, | |
66 | false, | |
67 | false, | |
68 | }, | |
69 | { | |
70 | `15 19 0:4 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw | |
71 | 12 19 8:2 /var/tmp /mountPoint4 rw,relatime,nodev - ext4 /dev/sda2 rw,discard,data=ordered | |
72 | 16 19 - sys rw | |
73 | 17 19 0:6 / /dev rw,nosuid,relatime shared:2 - devtmpfs dev rw,size=6089152k,nr_inodes=1522288,mode=755 | |
74 | 18 19 0:16 / /run rw,nosuid,nodev,relatime shared:12 - tmpfs run rw,mode=755 | |
75 | 19 0 254:0 / / rw,relatime shared:1 - ext4 /dev/mapper/cryptroot rw,discard,data=ordered | |
76 | 20 16 0:17 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:7 - securityfs securityfs rw | |
77 | 21 17 0:18 / /dev/shm rw,nosuid,nodev shared:3 - tmpfs tmpfs rw`, | |
78 | "/mountPoint4", | |
79 | false, | |
80 | false, | |
81 | true, | |
82 | }, | |
83 | } | |
84 | ||
85 | func TestCheckMountLinux(t *testing.T) { | |
86 | for i, mi := range mountinfoTests { | |
87 | isMounted, ro, err := parseMountinfo(strings.NewReader(mi.mountinfo), mi.mountPoint) | |
88 | if err != nil { | |
89 | if mi.expectErr { | |
90 | continue | |
91 | } else { | |
92 | t.Fatalf("#%d: unexpected error: `%v`", i, err) | |
93 | } | |
94 | } | |
95 | if isMounted != mi.expectMounted { | |
96 | t.Fatalf("#%d: expected isMounted=`%v` but got `%v`", i, mi.expectMounted, isMounted) | |
97 | } | |
98 | if ro != mi.readOnly { | |
99 | t.Fatalf("#%d: expected readOnly=`%v` but got `%v`", i, mi.readOnly, ro) | |
100 | } | |
101 | } | |
102 | } |
22 | 22 | "github.com/appc/spec/pkg/tarheader" |
23 | 23 | ) |
24 | 24 | |
25 | // TarHeaderWalkFunc is the type of the function which allows setting tar | |
26 | // headers or filtering out tar entries when building an ACI. It will be | |
27 | // applied to every entry in the tar file. | |
28 | // | |
29 | // If true is returned, the entry will be included in the final ACI; if false, | |
30 | // the entry will not be included. | |
31 | type TarHeaderWalkFunc func(hdr *tar.Header) bool | |
32 | ||
25 | 33 | // BuildWalker creates a filepath.WalkFunc that walks over the given root |
26 | 34 | // (which should represent an ACI layout on disk) and adds the files in the |
27 | 35 | // rootfs/ subdirectory to the given ArchiveWriter |
28 | func BuildWalker(root string, aw ArchiveWriter) filepath.WalkFunc { | |
36 | func BuildWalker(root string, aw ArchiveWriter, cb TarHeaderWalkFunc) filepath.WalkFunc { | |
29 | 37 | // cache of inode -> filepath, used to leverage hard links in the archive |
30 | 38 | inos := map[uint64]string{} |
31 | 39 | return func(path string, info os.FileInfo, err error) error { |
85 | 93 | hdr.Size = 0 |
86 | 94 | r = nil |
87 | 95 | } |
96 | ||
97 | if cb != nil { | |
98 | if !cb(hdr) { | |
99 | return nil | |
100 | } | |
101 | } | |
102 | ||
88 | 103 | if err := aw.AddFile(hdr, r); err != nil { |
89 | 104 | return err |
90 | 105 | } |
102 | 102 | } |
103 | 103 | } |
104 | 104 | |
105 | // XzReader shells out to a command line xz executable (if | |
105 | // XzReader is an io.ReadCloser which decompresses xz compressed data. | |
106 | type XzReader struct { | |
107 | io.ReadCloser | |
108 | cmd *exec.Cmd | |
109 | closech chan error | |
110 | } | |
111 | ||
112 | // NewXzReader shells out to a command line xz executable (if | |
106 | 113 | // available) to decompress the given io.Reader using the xz |
107 | // compression format | |
108 | func XzReader(r io.Reader) io.ReadCloser { | |
114 | // compression format and returns an *XzReader. | |
115 | // It is the caller's responsibility to call Close on the XzReader when done. | |
116 | func NewXzReader(r io.Reader) (*XzReader, error) { | |
109 | 117 | rpipe, wpipe := io.Pipe() |
110 | 118 | ex, err := exec.LookPath("xz") |
111 | 119 | if err != nil { |
112 | 120 | log.Fatalf("couldn't find xz executable: %v", err) |
113 | 121 | } |
114 | 122 | cmd := exec.Command(ex, "--decompress", "--stdout") |
123 | ||
124 | closech := make(chan error) | |
125 | ||
115 | 126 | cmd.Stdin = r |
116 | 127 | cmd.Stdout = wpipe |
117 | 128 | |
118 | 129 | go func() { |
119 | 130 | err := cmd.Run() |
120 | 131 | wpipe.CloseWithError(err) |
132 | closech <- err | |
121 | 133 | }() |
122 | 134 | |
123 | return rpipe | |
135 | return &XzReader{rpipe, cmd, closech}, nil | |
136 | } | |
137 | ||
138 | func (r *XzReader) Close() error { | |
139 | r.ReadCloser.Close() | |
140 | r.cmd.Process.Kill() | |
141 | return <-r.closech | |
124 | 142 | } |
125 | 143 | |
126 | 144 | // ManifestFromImage extracts a new schema.ImageManifest from the given ACI image. |
131 | 149 | if err != nil { |
132 | 150 | return nil, err |
133 | 151 | } |
152 | defer tr.Close() | |
134 | 153 | |
135 | 154 | for { |
136 | 155 | hdr, err := tr.Next() |
154 | 173 | } |
155 | 174 | } |
156 | 175 | |
157 | // NewCompressedTarReader creates a new tar.Reader reading from the given ACI image. | |
158 | func NewCompressedTarReader(rs io.ReadSeeker) (*tar.Reader, error) { | |
176 | // TarReadCloser embeds a *tar.Reader and the related io.Closer | |
177 | // It is the caller's responsibility to call Close on TarReadCloser when | |
178 | // done. | |
179 | type TarReadCloser struct { | |
180 | *tar.Reader | |
181 | io.Closer | |
182 | } | |
183 | ||
184 | func (r *TarReadCloser) Close() error { | |
185 | return r.Closer.Close() | |
186 | } | |
187 | ||
188 | // NewCompressedTarReader creates a new TarReadCloser reading from the | |
189 | // given ACI image. | |
190 | // It is the caller's responsibility to call Close on the TarReadCloser | |
191 | // when done. | |
192 | func NewCompressedTarReader(rs io.ReadSeeker) (*TarReadCloser, error) { | |
159 | 193 | cr, err := NewCompressedReader(rs) |
160 | 194 | if err != nil { |
161 | 195 | return nil, err |
162 | 196 | } |
163 | return tar.NewReader(cr), nil | |
164 | } | |
165 | ||
166 | // NewCompressedReader creates a new io.Reader from the given ACI image. | |
167 | func NewCompressedReader(rs io.ReadSeeker) (io.Reader, error) { | |
197 | return &TarReadCloser{tar.NewReader(cr), cr}, nil | |
198 | } | |
199 | ||
200 | // NewCompressedReader creates a new io.ReaderCloser from the given ACI image. | |
201 | // It is the caller's responsibility to call Close on the Reader when done. | |
202 | func NewCompressedReader(rs io.ReadSeeker) (io.ReadCloser, error) { | |
168 | 203 | |
169 | 204 | var ( |
170 | dr io.Reader | |
205 | dr io.ReadCloser | |
171 | 206 | err error |
172 | 207 | ) |
173 | 208 | |
193 | 228 | return nil, err |
194 | 229 | } |
195 | 230 | case TypeBzip2: |
196 | dr = bzip2.NewReader(rs) | |
231 | dr = ioutil.NopCloser(bzip2.NewReader(rs)) | |
197 | 232 | case TypeXz: |
198 | dr = XzReader(rs) | |
233 | dr, err = NewXzReader(rs) | |
234 | if err != nil { | |
235 | return nil, err | |
236 | } | |
199 | 237 | case TypeTar: |
200 | dr = rs | |
238 | dr = ioutil.NopCloser(rs) | |
201 | 239 | case TypeUnknown: |
202 | 240 | return nil, errors.New("error: unknown image filetype") |
203 | 241 | default: |
27 | 27 | return nil, err |
28 | 28 | } |
29 | 29 | |
30 | manifestBody := `{"acKind":"ImageManifest","acVersion":"0.6.1","name":"example.com/app"}` | |
30 | manifestBody := `{"acKind":"ImageManifest","acVersion":"0.7.1","name":"example.com/app"}` | |
31 | 31 | |
32 | 32 | gw := gzip.NewWriter(tf) |
33 | 33 | tw := tar.NewWriter(gw) |
39 | 39 | "strings" |
40 | 40 | |
41 | 41 | "github.com/appc/spec/schema" |
42 | "github.com/appc/spec/schema/types" | |
42 | 43 | ) |
43 | 44 | |
44 | 45 | const ( |
47 | 48 | // Path to rootfs directory inside the layout |
48 | 49 | RootfsDir = "rootfs" |
49 | 50 | ) |
51 | ||
52 | type ErrOldVersion struct { | |
53 | version types.SemVer | |
54 | } | |
55 | ||
56 | func (e ErrOldVersion) Error() string { | |
57 | return fmt.Sprintf("ACVersion too old. Found major version %v, expected %v", e.version.Major, schema.AppContainerVersion.Major) | |
58 | } | |
50 | 59 | |
51 | 60 | var ( |
52 | 61 | ErrNoRootFS = errors.New("no rootfs found in layout") |
163 | 172 | if err := a.UnmarshalJSON(b); err != nil { |
164 | 173 | return fmt.Errorf("image manifest validation failed: %v", err) |
165 | 174 | } |
175 | if a.ACVersion.LessThanMajor(schema.AppContainerVersion) { | |
176 | return ErrOldVersion{ | |
177 | version: a.ACVersion, | |
178 | } | |
179 | } | |
166 | 180 | for _, f := range files { |
167 | 181 | if !strings.HasPrefix(f, "rootfs") { |
168 | 182 | return fmt.Errorf("unrecognized file path in layout: %q", f) |
14 | 14 | package aci |
15 | 15 | |
16 | 16 | import ( |
17 | "fmt" | |
17 | 18 | "io/ioutil" |
18 | 19 | "os" |
19 | 20 | "path" |
20 | 21 | "testing" |
22 | ||
23 | "github.com/appc/spec/schema" | |
21 | 24 | ) |
22 | 25 | |
23 | 26 | func newValidateLayoutTest() (string, error) { |
35 | 38 | } |
36 | 39 | |
37 | 40 | evilManifestBody := "malformedManifest" |
38 | manifestBody := `{"acKind":"ImageManifest","acVersion":"0.3.0","name":"example.com/app"}` | |
41 | manifestBody := fmt.Sprintf(`{"acKind":"ImageManifest","acVersion":"%s","name":"example.com/app"}`, schema.AppContainerVersion) | |
39 | 42 | |
40 | 43 | evilManifestPath := "rootfs/manifest" |
41 | 44 | evilManifestPath = path.Join(td, evilManifestPath) |
28 | 28 | var ( |
29 | 29 | buildNocompress bool |
30 | 30 | buildOverwrite bool |
31 | buildOwnerRoot bool | |
31 | 32 | cmdBuild = &Command{ |
32 | 33 | Name: "build", |
33 | 34 | Description: `Build an ACI from a given directory. The directory should |
35 | 36 | before the ACI is created. The produced ACI will be |
36 | 37 | gzip-compressed by default.`, |
37 | 38 | Summary: "Build an ACI from an Image Layout (experimental)", |
38 | Usage: `[--overwrite] [--no-compression] DIRECTORY OUTPUT_FILE`, | |
39 | Usage: `[--overwrite] [--no-compression] [--owner-root] DIRECTORY OUTPUT_FILE`, | |
39 | 40 | Run: runBuild, |
40 | 41 | } |
41 | 42 | ) |
42 | 43 | |
43 | 44 | func init() { |
44 | 45 | cmdBuild.Flags.BoolVar(&buildOverwrite, "overwrite", false, "Overwrite target file if it already exists") |
46 | cmdBuild.Flags.BoolVar(&buildOwnerRoot, "owner-root", false, "Force ownership to root:root on all files") | |
45 | 47 | cmdBuild.Flags.BoolVar(&buildNocompress, "no-compression", false, "Do not gzip-compress the produced ACI") |
46 | 48 | } |
47 | 49 | |
57 | 59 | if ext != schema.ACIExtension { |
58 | 60 | stderr("build: Extension must be %s (given %s)", schema.ACIExtension, ext) |
59 | 61 | return 1 |
62 | } | |
63 | ||
64 | // TODO(jonboulle): stream the validation so we don't have to walk the rootfs twice | |
65 | if err := aci.ValidateLayout(root); err != nil { | |
66 | if e, ok := err.(aci.ErrOldVersion); ok { | |
67 | stderr("build: Warning: %v. Please update your manifest.", e) | |
68 | } else { | |
69 | stderr("build: Layout failed validation: %v", err) | |
70 | return 1 | |
71 | } | |
60 | 72 | } |
61 | 73 | |
62 | 74 | mode := os.O_CREATE | os.O_WRONLY |
94 | 106 | } |
95 | 107 | }() |
96 | 108 | |
97 | // TODO(jonboulle): stream the validation so we don't have to walk the rootfs twice | |
98 | if err := aci.ValidateLayout(root); err != nil { | |
99 | stderr("build: Layout failed validation: %v", err) | |
100 | return 1 | |
101 | } | |
102 | 109 | mpath := filepath.Join(root, aci.ManifestFile) |
103 | 110 | b, err := ioutil.ReadFile(mpath) |
104 | 111 | if err != nil { |
112 | 119 | } |
113 | 120 | iw := aci.NewImageWriter(im, tr) |
114 | 121 | |
115 | err = filepath.Walk(root, aci.BuildWalker(root, iw)) | |
122 | var walkerCb aci.TarHeaderWalkFunc | |
123 | if buildOwnerRoot { | |
124 | walkerCb = func(hdr *tar.Header) bool { | |
125 | hdr.Uid, hdr.Gid = 0, 0 | |
126 | hdr.Uname, hdr.Gname = "root", "root" | |
127 | return true | |
128 | } | |
129 | } | |
130 | ||
131 | err = filepath.Walk(root, aci.BuildWalker(root, iw, walkerCb)) | |
116 | 132 | if err != nil { |
117 | 133 | stderr("build: Error walking rootfs: %v", err) |
118 | 134 | return 1 |
24 | 24 | "os" |
25 | 25 | "path" |
26 | 26 | "path/filepath" |
27 | "strconv" | |
27 | 28 | "strings" |
28 | 29 | |
29 | 30 | "github.com/appc/spec/aci" |
35 | 36 | inputFile string |
36 | 37 | outputFile string |
37 | 38 | |
38 | patchNocompress bool | |
39 | patchOverwrite bool | |
40 | patchReplace bool | |
41 | patchManifestFile string | |
42 | patchName string | |
43 | patchExec string | |
44 | patchUser string | |
45 | patchGroup string | |
46 | patchCaps string | |
47 | patchMounts string | |
48 | patchPorts string | |
49 | patchIsolators string | |
39 | patchNocompress bool | |
40 | patchOverwrite bool | |
41 | patchReplace bool | |
42 | patchManifestFile string | |
43 | patchName string | |
44 | patchExec string | |
45 | patchUser string | |
46 | patchGroup string | |
47 | patchSupplementaryGIDs string | |
48 | patchCaps string | |
49 | patchMounts string | |
50 | patchPorts string | |
51 | patchIsolators string | |
50 | 52 | |
51 | 53 | catPrettyPrint bool |
52 | 54 | |
62 | 64 | [--capability=CAP_SYS_ADMIN,CAP_NET_ADMIN] |
63 | 65 | [--mounts=work,path=/opt,readOnly=true[:work2,...]] |
64 | 66 | [--ports=query,protocol=tcp,port=8080[:query2,...]] |
65 | [--isolators=resource/cpu,request=50,limit=100[:resource/memory,...]] | |
67 | [--supplementary-groups=gid1,gid2,...] | |
68 | [--isolators=resource/cpu,request=50m,limit=100m[:resource/memory,...]] | |
66 | 69 | [--replace] |
67 | 70 | INPUT_ACI_FILE |
68 | 71 | [OUTPUT_ACI_FILE]`, |
87 | 90 | cmdPatchManifest.Flags.StringVar(&patchExec, "exec", "", "Replace the command line to launch the executable") |
88 | 91 | cmdPatchManifest.Flags.StringVar(&patchUser, "user", "", "Replace user") |
89 | 92 | cmdPatchManifest.Flags.StringVar(&patchGroup, "group", "", "Replace group") |
93 | cmdPatchManifest.Flags.StringVar(&patchSupplementaryGIDs, "supplementary-groups", "", "Replace supplementary groups, expects a comma-separated list.") | |
90 | 94 | cmdPatchManifest.Flags.StringVar(&patchCaps, "capability", "", "Replace capability") |
91 | 95 | cmdPatchManifest.Flags.StringVar(&patchMounts, "mounts", "", "Replace mount points") |
92 | 96 | cmdPatchManifest.Flags.StringVar(&patchPorts, "ports", "", "Replace ports") |
144 | 148 | im.Name = *name |
145 | 149 | } |
146 | 150 | |
151 | var app *types.App = im.App | |
147 | 152 | if patchExec != "" { |
148 | im.App.Exec = strings.Split(patchExec, " ") | |
153 | if app == nil { | |
154 | // if the original manifest was missing an app and | |
155 | // patchExec is set let's assume the user is trying to | |
156 | // inject one... | |
157 | im.App = &types.App{} | |
158 | app = im.App | |
159 | } | |
160 | app.Exec = strings.Split(patchExec, " ") | |
161 | } | |
162 | ||
163 | if patchUser != "" || patchGroup != "" || patchSupplementaryGIDs != "" || patchCaps != "" || patchMounts != "" || patchPorts != "" || patchIsolators != "" { | |
164 | // ...but if we still don't have an app and the user is trying | |
165 | // to patch one of its other parameters, it's an error | |
166 | if app == nil { | |
167 | return fmt.Errorf("no app in the supplied manifest and no exec command provided") | |
168 | } | |
149 | 169 | } |
150 | 170 | |
151 | 171 | if patchUser != "" { |
152 | im.App.User = patchUser | |
153 | } | |
172 | app.User = patchUser | |
173 | } | |
174 | ||
154 | 175 | if patchGroup != "" { |
155 | im.App.Group = patchGroup | |
156 | } | |
157 | ||
158 | var app *types.App | |
159 | if patchCaps != "" || patchMounts != "" || patchPorts != "" || patchIsolators != "" { | |
160 | app = im.App | |
161 | if app == nil { | |
162 | return fmt.Errorf("no app in the manifest") | |
176 | app.Group = patchGroup | |
177 | } | |
178 | ||
179 | if patchSupplementaryGIDs != "" { | |
180 | app.SupplementaryGIDs = []int{} | |
181 | gids := strings.Split(patchSupplementaryGIDs, ",") | |
182 | for _, g := range gids { | |
183 | gid, err := strconv.Atoi(g) | |
184 | if err != nil { | |
185 | return fmt.Errorf("invalid supplementary group %q: %v", g, err) | |
186 | } | |
187 | app.SupplementaryGIDs = append(app.SupplementaryGIDs, gid) | |
163 | 188 | } |
164 | 189 | } |
165 | 190 | |
403 | 428 | stderr("patch-manifest: Cannot extract %s: %v", inputFile, err) |
404 | 429 | return 1 |
405 | 430 | } |
431 | defer tr.Close() | |
406 | 432 | |
407 | 433 | var newManifest []byte |
408 | 434 | |
421 | 447 | } |
422 | 448 | } |
423 | 449 | |
424 | err = extractManifest(tr, tw, false, newManifest) | |
450 | err = extractManifest(tr.Reader, tw, false, newManifest) | |
425 | 451 | if err != nil { |
426 | 452 | stderr("patch-manifest: Unable to read %s: %v", inputFile, err) |
427 | 453 | return 1 |
458 | 484 | stderr("cat-manifest: Cannot extract %s: %v", inputFile, err) |
459 | 485 | return 1 |
460 | 486 | } |
461 | ||
462 | err = extractManifest(tr, nil, true, nil) | |
487 | defer tr.Close() | |
488 | ||
489 | err = extractManifest(tr.Reader, nil, true, nil) | |
463 | 490 | if err != nil { |
464 | 491 | stderr("cat-manifest: Unable to read %s: %v", inputFile, err) |
465 | 492 | return 1 |
14 | 14 | package main |
15 | 15 | |
16 | 16 | import ( |
17 | "archive/tar" | |
18 | "compress/bzip2" | |
19 | "compress/gzip" | |
20 | "errors" | |
21 | 17 | "fmt" |
22 | "io" | |
23 | 18 | "io/ioutil" |
24 | 19 | "os" |
25 | 20 | "strings" |
106 | 101 | stderr("%s: valid image layout", path) |
107 | 102 | } |
108 | 103 | case typeAppImage: |
109 | fr, err := maybeDecompress(fh) | |
104 | tr, err := aci.NewCompressedTarReader(fh) | |
110 | 105 | if err != nil { |
111 | 106 | stderr("%s: error decompressing file: %v", path, err) |
112 | 107 | return 1 |
113 | 108 | } |
114 | tr := tar.NewReader(fr) | |
115 | err = aci.ValidateArchive(tr) | |
109 | err = aci.ValidateArchive(tr.Reader) | |
110 | tr.Close() | |
116 | 111 | fh.Close() |
117 | 112 | if err != nil { |
118 | stderr("%s: error validating: %v", path, err) | |
119 | exit = 1 | |
113 | if e, ok := err.(aci.ErrOldVersion); ok { | |
114 | stderr("%s: warning: %v", path, e) | |
115 | } else { | |
116 | stderr("%s: error validating: %v", path, err) | |
117 | exit = 1 | |
118 | } | |
120 | 119 | } else if globalFlags.Debug { |
121 | 120 | stderr("%s: valid app container image", path) |
122 | 121 | } |
175 | 174 | return "", nil |
176 | 175 | } |
177 | 176 | } |
178 | ||
179 | func maybeDecompress(rs io.ReadSeeker) (io.Reader, error) { | |
180 | // TODO(jonboulle): this is a bit redundant with detectValType | |
181 | typ, err := aci.DetectFileType(rs) | |
182 | if err != nil { | |
183 | return nil, err | |
184 | } | |
185 | if _, err := rs.Seek(0, 0); err != nil { | |
186 | return nil, err | |
187 | } | |
188 | var r io.Reader | |
189 | switch typ { | |
190 | case aci.TypeGzip: | |
191 | r, err = gzip.NewReader(rs) | |
192 | if err != nil { | |
193 | return nil, fmt.Errorf("error reading gzip: %v", err) | |
194 | } | |
195 | case aci.TypeBzip2: | |
196 | r = bzip2.NewReader(rs) | |
197 | case aci.TypeXz: | |
198 | r = aci.XzReader(rs) | |
199 | case aci.TypeTar: | |
200 | r = rs | |
201 | case aci.TypeUnknown: | |
202 | return nil, errors.New("unknown filetype") | |
203 | default: | |
204 | // should never happen | |
205 | panic(fmt.Sprintf("bad type returned from DetectFileType: %v", typ)) | |
206 | } | |
207 | return r, nil | |
208 | } |
181 | 181 | pre := strings.Join(parts[:end], "/") |
182 | 182 | |
183 | 183 | eps, err = doDiscover(pre, app, insecure) |
184 | derr := discoverFn(pre, eps, err) | |
185 | if derr != nil { | |
186 | return err | |
184 | if derr := discoverFn(pre, eps, err); derr != nil { | |
185 | return derr | |
187 | 186 | } |
188 | 187 | } |
189 | 188 |
45 | 45 | // Example app parameters: |
46 | 46 | // example.com/reduce-worker:1.0.0 |
47 | 47 | // example.com/reduce-worker,channel=alpha,label=value |
48 | // example.com/reduce-worker:1.0.0,label=value | |
49 | // | |
50 | // As can be seen in above examples - colon, comma and equal sign have | |
51 | // special meaning. If any of them has to be a part of a label's value | |
52 | // then consider writing your own string to App parser. | |
48 | 53 | func NewAppFromString(app string) (*App, error) { |
49 | 54 | var ( |
50 | 55 | name string |
51 | 56 | labels map[types.ACIdentifier]string |
52 | 57 | ) |
53 | 58 | |
54 | app = strings.Replace(app, ":", ",version=", -1) | |
55 | app = "name=" + app | |
56 | v, err := url.ParseQuery(strings.Replace(app, ",", "&", -1)) | |
59 | preparedApp, err := prepareAppString(app) | |
60 | if err != nil { | |
61 | return nil, err | |
62 | } | |
63 | v, err := url.ParseQuery(preparedApp) | |
57 | 64 | if err != nil { |
58 | 65 | return nil, err |
59 | 66 | } |
79 | 86 | return a, nil |
80 | 87 | } |
81 | 88 | |
89 | func prepareAppString(app string) (string, error) { | |
90 | if err := checkColon(app); err != nil { | |
91 | return "", err | |
92 | } | |
93 | ||
94 | app = "name=" + strings.Replace(app, ":", ",version=", 1) | |
95 | return makeQueryString(app) | |
96 | } | |
97 | ||
98 | func checkColon(app string) error { | |
99 | firstComma := strings.IndexRune(app, ',') | |
100 | firstColon := strings.IndexRune(app, ':') | |
101 | if firstColon > firstComma && firstComma > -1 { | |
102 | return fmt.Errorf("malformed app string - colon may appear only right after the app name") | |
103 | } | |
104 | if strings.Count(app, ":") > 1 { | |
105 | return fmt.Errorf("malformed app string - colon may appear at most once") | |
106 | } | |
107 | return nil | |
108 | } | |
109 | ||
110 | func makeQueryString(app string) (string, error) { | |
111 | parts := strings.Split(app, ",") | |
112 | escapedParts := make([]string, len(parts)) | |
113 | for i, s := range parts { | |
114 | p := strings.SplitN(s, "=", 2) | |
115 | if len(p) != 2 { | |
116 | return "", fmt.Errorf("malformed app string - has a label without a value: %s", p[0]) | |
117 | } | |
118 | escapedParts[i] = fmt.Sprintf("%s=%s", p[0], url.QueryEscape(p[1])) | |
119 | } | |
120 | return strings.Join(escapedParts, "&"), nil | |
121 | } | |
122 | ||
82 | 123 | func (a *App) Copy() *App { |
83 | 124 | ac := &App{ |
84 | 125 | Name: a.Name, |
51 | 51 | |
52 | 52 | false, |
53 | 53 | }, |
54 | { | |
55 | "example.com/app:1.2.3,special=!*'();@&+$/?#[],channel=beta", | |
56 | ||
57 | &App{ | |
58 | Name: "example.com/app", | |
59 | Labels: map[types.ACIdentifier]string{ | |
60 | "version": "1.2.3", | |
61 | "special": "!*'();@&+$/?#[]", | |
62 | "channel": "beta", | |
63 | }, | |
64 | }, | |
65 | ||
66 | false, | |
67 | }, | |
54 | 68 | |
55 | 69 | // bad AC name for app |
56 | 70 | { |
71 | 85 | // multi-value labels |
72 | 86 | { |
73 | 87 | "foo.com/bar,channel=alpha,dog=woof,channel=beta", |
88 | ||
89 | nil, | |
90 | true, | |
91 | }, | |
92 | // colon coming after some label instead of being | |
93 | // right after the name | |
94 | { | |
95 | "example.com/app,channel=beta:1.2.3", | |
96 | ||
97 | nil, | |
98 | true, | |
99 | }, | |
100 | // two colons in string | |
101 | { | |
102 | "example.com/app:3.2.1,channel=beta:1.2.3", | |
103 | ||
104 | nil, | |
105 | true, | |
106 | }, | |
107 | // two version labels, one implicit, one explicit | |
108 | { | |
109 | "example.com/app:3.2.1,version=1.2.3", | |
74 | 110 | |
75 | 111 | nil, |
76 | 112 | true, |
0 | 0 | { |
1 | 1 | "acKind": "ImageManifest", |
2 | "acVersion": "0.6.1", | |
2 | "acVersion": "0.7.1", | |
3 | 3 | "name": "example.com/reduce-worker", |
4 | 4 | "labels": [ |
5 | 5 | { |
46 | 46 | "isolators": [ |
47 | 47 | { |
48 | 48 | "name": "resource/cpu", |
49 | "value": {"limit": "20"} | |
49 | "value": {"limit": "20m"} | |
50 | 50 | }, |
51 | 51 | { |
52 | 52 | "name": "resource/memory", |
0 | 0 | { |
1 | "acVersion": "0.6.1", | |
2 | 1 | "acKind": "PodManifest", |
2 | "acVersion": "0.7.1", | |
3 | 3 | "apps": [ |
4 | 4 | { |
5 | 5 | "name": "reduce-worker", |
9 | 9 | }, |
10 | 10 | "app": { |
11 | 11 | "exec": [ |
12 | "/bin/reduce-worker", | |
12 | "/usr/bin/reduce-worker", | |
13 | 13 | "--debug=true", |
14 | 14 | "--data-dir=/mnt/foo" |
15 | 15 | ], |
17 | 17 | "user": "0", |
18 | 18 | "mountPoints": [ |
19 | 19 | { |
20 | "name": "work", | |
21 | "path": "/mnt/foo" | |
20 | "name": "database", | |
21 | "path": "/var/lib/db" | |
22 | 22 | } |
23 | 23 | ], |
24 | 24 | "isolators": [ |
29 | 29 | ] |
30 | 30 | }, |
31 | 31 | "mounts": [ |
32 | {"volume": "work", "mountPoint": "work"} | |
32 | { | |
33 | "volume": "dbvolume", | |
34 | "path": "/var/lib/db" | |
35 | } | |
33 | 36 | ] |
34 | 37 | }, |
35 | 38 | { |
45 | 48 | ] |
46 | 49 | }, |
47 | 50 | "mounts": [ |
48 | {"volume": "database", "mountPoint": "db"}, | |
49 | {"volume": "buildoutput", "mountPoint": "out"} | |
51 | { | |
52 | "volume": "dbvolume", | |
53 | "path": "/mnt/db" | |
54 | }, | |
55 | { | |
56 | "volume": "buildoutput", | |
57 | "path": "/mnt/out" | |
58 | } | |
50 | 59 | ], |
51 | 60 | "annotations": [ |
52 | 61 | { |
71 | 80 | ], |
72 | 81 | "volumes": [ |
73 | 82 | { |
74 | "name": "database", | |
83 | "name": "dbvolume", | |
75 | 84 | "kind": "host", |
76 | 85 | "source": "/opt/tenant1/database", |
77 | 86 | "readOnly": true |
Binary diff not shown
0 | <?xml version="1.0" encoding="utf-8"?> | |
1 | <!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> | |
2 | <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | |
3 | viewBox="0 0 255.3 72" style="enable-background:new 0 0 255.3 72;" xml:space="preserve"> | |
4 | <g> | |
5 | <g> | |
6 | <path d="M115.2,29.5h2.3v-1.1c0-8.1-4.4-10.8-10.4-10.8c-6.9,0-11.8,4-11.8,4l-2.7-4.3c0,0,5.5-4.7,14.9-4.7 | |
7 | c10.1,0,15.6,5.6,15.6,16v26.8h-5.4v-4.6c0-2.1,0.2-3.5,0.2-3.5h-0.2c0,0-3.5,9.1-14,9.1c-7,0-14.2-4.1-14.2-12.4 | |
8 | C89.6,29.8,107.9,29.5,115.2,29.5z M104.7,51.5c8,0,12.8-8.3,12.8-15.5v-1.8h-2.4c-6.6,0-19.7,0.2-19.7,9.5 | |
9 | C95.4,47.6,98.5,51.5,104.7,51.5z"/> | |
10 | <path d="M130.1,13.5h5.4v4.1c0,1.8-0.2,3.2-0.2,3.2h0.2c0,0,3.4-8.3,14.5-8.3c11.2,0,18.2,9,18.2,22c0,13.2-7.9,21.9-18.7,21.9 | |
11 | c-10.1,0-13.7-8-13.7-8h-0.2c0,0,0.2,1.5,0.2,3.7v20h-5.7V13.5z M148.9,51.4c7.4,0,13.5-6.2,13.5-16.9c0-10.2-5.4-16.8-13.2-16.8 | |
12 | c-7,0-13.6,5-13.6,16.9C135.6,43,140.3,51.4,148.9,51.4z"/> | |
13 | <path d="M174.4,13.5h5.4v4.1c0,1.8-0.2,3.2-0.2,3.2h0.2c0,0,3.4-8.3,14.5-8.3c11.2,0,18.2,9,18.2,22c0,13.2-7.9,21.9-18.7,21.9 | |
14 | c-10.1,0-13.7-8-13.7-8h-0.2c0,0,0.2,1.5,0.2,3.7v20h-5.7V13.5z M193.2,51.4c7.4,0,13.5-6.2,13.5-16.9c0-10.2-5.4-16.8-13.2-16.8 | |
15 | c-7,0-13.6,5-13.6,16.9C179.8,43,184.5,51.4,193.2,51.4z"/> | |
16 | <path d="M239,12.5c10.2,0,15.3,6,15.3,6l-2.9,4.2c0,0-4.7-5.1-12.2-5.1c-9.1,0-16.3,6.8-16.3,16.8c0,9.8,7.2,16.8,16.5,16.8 | |
17 | c8.5,0,13.4-5.9,13.4-5.9l2.6,4.4c0,0-5.7,6.6-16.3,6.6c-12.4,0-22-8.9-22-21.9C217,21.6,226.7,12.5,239,12.5z"/> | |
18 | </g> | |
19 | <path d="M66.9,52l-2.3-3.9h-4.5L57.8,52l1.6,2.8h-3.2l-1.6,2.8l-3.1-5.4H44l-3.1,5.4l-3.1-5.4h-6.2l5-8.7l-5.7-9.8h-10l4.3-7.5 | |
20 | l3.1,5.4h7.5l3.8-6.5l-3.1-5.4h10l5-8.7l1.6,2.8h3.2l-1.6,2.8l2.3,3.9h4.5l2.3-3.9l-2.3-3.9h-3.2L60,9.8l-2.3-3.9h-4.5l-1.6,2.8 | |
21 | l-5-8.7H35.2l-5,8.7l-3.1-5.4h-7.5l-3.8,6.5l3.1,5.4h-10L3.2,25l5.7,9.8h10l-4.3,7.5l-3.1-5.4H3.8L0,43.5l3.8,6.6h7.6l3.1-5.4 | |
22 | l4.3,7.5h-3.7l-2.5,4.4l2.5,4.4h5l2.5-4.4l-1.9-3.2h8.7l-3.1,5.4l3.8,6.5h6.2L34.9,68l2.3,3.9h4.5l2.3-3.9l-2.3-3.9h-3.2l2.4-4.2 | |
23 | l3.1,5.4h7.5l3.1-5.4l1.6,2.8h4.5l2.3-3.9l-1.6-2.8h3.2L66.9,52z M20.9,16.4h6.2l3.1-5.4l4.3,7.5h-6.2l-3.1,5.4L20.9,16.4z | |
24 | M35.2,30.4H29L25.9,25l3.1-5.4h6.2l3.1,5.4L35.2,30.4z M60.9,13.8l1.6,2.8l-1.6,2.8h-3.2l-1.6-2.8l1.6-2.8h0l0,0H60.9z M53.8,7.1 | |
25 | H57l1.6,2.8L57,12.6h0l0,0h-3.2l-1.6-2.8L53.8,7.1z M35.9,1.2h10l5,8.7l-5,8.7h-10l-5-8.7L35.9,1.2z M17.2,9.8l3.1-5.4h6.2l3.1,5.4 | |
26 | l-3.1,5.4h-6.2h0L17.2,9.8z M9.6,33.7l-5-8.7l5-8.7h10h0l5,8.7l-5,8.7h0H9.6z M10.8,48.9H4.5l-3.1-5.4l3.1-5.4h6.3l3.1,5.4 | |
27 | L10.8,48.9z M20.3,34.9L20.3,34.9h10l5,8.7l-5,8.7h-10l-5-8.7L20.3,34.9z M19.6,59.8h-3.7L14,56.6l1.8-3.2h3.7l1.8,3.2L19.6,59.8z | |
28 | M27.8,58.7l3.1-5.4h6.2l3.1,5.4l-3.1,5.4h-6.2L27.8,58.7z M42.6,68L41,70.7h-3.2L36.2,68l1.6-2.8H41L42.6,68z M50.9,64.1h-6.2 | |
29 | l-3.1-5.4l3.1-5.4h6.2l3.1,5.4L50.9,64.1z M61.7,58.7l-1.6,2.8h-3.2l-1.6-2.8l1.6-2.8h3.2l0,0h0L61.7,58.7z M60.8,54.8L60.8,54.8 | |
30 | L60.8,54.8L59.2,52l1.6-2.8h3.2l1.6,2.8l-1.6,2.8H60.8z"/> | |
31 | </g> | |
32 | </svg> |
Binary diff not shown
0 | <?xml version="1.0" encoding="utf-8"?> | |
1 | <!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> | |
2 | <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | |
3 | viewBox="0 0 255.3 72" style="enable-background:new 0 0 255.3 72;" xml:space="preserve"> | |
4 | <g> | |
5 | <g> | |
6 | <path style="fill:#F38B00;" d="M115.2,29.5h2.3v-1.1c0-8.1-4.4-10.8-10.4-10.8c-6.9,0-11.8,4-11.8,4l-2.7-4.3 | |
7 | c0,0,5.5-4.7,14.9-4.7c10.1,0,15.6,5.6,15.6,16v26.8h-5.4v-4.6c0-2.1,0.2-3.5,0.2-3.5h-0.2c0,0-3.5,9.1-14,9.1 | |
8 | c-7,0-14.2-4.1-14.2-12.4C89.6,29.8,107.9,29.5,115.2,29.5z M104.7,51.5c8,0,12.8-8.3,12.8-15.5v-1.8h-2.4 | |
9 | c-6.6,0-19.7,0.2-19.7,9.5C95.4,47.6,98.5,51.5,104.7,51.5z"/> | |
10 | <path style="fill:#F38B00;" d="M130.1,13.5h5.4v4.1c0,1.8-0.2,3.2-0.2,3.2h0.2c0,0,3.4-8.3,14.5-8.3c11.2,0,18.2,9,18.2,22 | |
11 | c0,13.2-7.9,21.9-18.7,21.9c-10.1,0-13.7-8-13.7-8h-0.2c0,0,0.2,1.5,0.2,3.7v20h-5.7V13.5z M148.9,51.4c7.4,0,13.5-6.2,13.5-16.9 | |
12 | c0-10.2-5.4-16.8-13.2-16.8c-7,0-13.6,5-13.6,16.9C135.6,43,140.3,51.4,148.9,51.4z"/> | |
13 | <path style="fill:#F38B00;" d="M174.4,13.5h5.4v4.1c0,1.8-0.2,3.2-0.2,3.2h0.2c0,0,3.4-8.3,14.5-8.3c11.2,0,18.2,9,18.2,22 | |
14 | c0,13.2-7.9,21.9-18.7,21.9c-10.1,0-13.7-8-13.7-8h-0.2c0,0,0.2,1.5,0.2,3.7v20h-5.7V13.5z M193.2,51.4c7.4,0,13.5-6.2,13.5-16.9 | |
15 | c0-10.2-5.4-16.8-13.2-16.8c-7,0-13.6,5-13.6,16.9C179.8,43,184.5,51.4,193.2,51.4z"/> | |
16 | <path style="fill:#F38B00;" d="M239,12.5c10.2,0,15.3,6,15.3,6l-2.9,4.2c0,0-4.7-5.1-12.2-5.1c-9.1,0-16.3,6.8-16.3,16.8 | |
17 | c0,9.8,7.2,16.8,16.5,16.8c8.5,0,13.4-5.9,13.4-5.9l2.6,4.4c0,0-5.7,6.6-16.3,6.6c-12.4,0-22-8.9-22-21.9 | |
18 | C217,21.6,226.7,12.5,239,12.5z"/> | |
19 | </g> | |
20 | <path style="fill:#FFB547;" d="M66.9,52l-2.3-3.9h-4.5L57.8,52l1.6,2.8h-3.2l-1.6,2.8l-3.1-5.4H44l-3.1,5.4l-3.1-5.4h-6.2l5-8.7 | |
21 | l-5.7-9.8h-10l4.3-7.5l3.1,5.4h7.5l3.8-6.5l-3.1-5.4h10l5-8.7l1.6,2.8h3.2l-1.6,2.8l2.3,3.9h4.5l2.3-3.9l-2.3-3.9h-3.2L60,9.8 | |
22 | l-2.3-3.9h-4.5l-1.6,2.8l-5-8.7H35.2l-5,8.7l-3.1-5.4h-7.5l-3.8,6.5l3.1,5.4h-10L3.2,25l5.7,9.8h10l-4.3,7.5l-3.1-5.4H3.8L0,43.5 | |
23 | l3.8,6.6h7.6l3.1-5.4l4.3,7.5h-3.7l-2.5,4.4l2.5,4.4h5l2.5-4.4l-1.9-3.2h8.7l-3.1,5.4l3.8,6.5h6.2L34.9,68l2.3,3.9h4.5l2.3-3.9 | |
24 | l-2.3-3.9h-3.2l2.4-4.2l3.1,5.4h7.5l3.1-5.4l1.6,2.8h4.5l2.3-3.9l-1.6-2.8h3.2L66.9,52z M20.9,16.4h6.2l3.1-5.4l4.3,7.5h-6.2 | |
25 | l-3.1,5.4L20.9,16.4z M35.2,30.4H29L25.9,25l3.1-5.4h6.2l3.1,5.4L35.2,30.4z M60.9,13.8l1.6,2.8l-1.6,2.8h-3.2l-1.6-2.8l1.6-2.8h0 | |
26 | l0,0H60.9z M53.8,7.1H57l1.6,2.8L57,12.6h0l0,0h-3.2l-1.6-2.8L53.8,7.1z M35.9,1.2h10l5,8.7l-5,8.7h-10l-5-8.7L35.9,1.2z M17.2,9.8 | |
27 | l3.1-5.4h6.2l3.1,5.4l-3.1,5.4h-6.2h0L17.2,9.8z M9.6,33.7l-5-8.7l5-8.7h10h0l5,8.7l-5,8.7h0H9.6z M10.8,48.9H4.5l-3.1-5.4l3.1-5.4 | |
28 | h6.3l3.1,5.4L10.8,48.9z M20.3,34.9L20.3,34.9h10l5,8.7l-5,8.7h-10l-5-8.7L20.3,34.9z M19.6,59.8h-3.7L14,56.6l1.8-3.2h3.7l1.8,3.2 | |
29 | L19.6,59.8z M27.8,58.7l3.1-5.4h6.2l3.1,5.4l-3.1,5.4h-6.2L27.8,58.7z M42.6,68L41,70.7h-3.2L36.2,68l1.6-2.8H41L42.6,68z | |
30 | M50.9,64.1h-6.2l-3.1-5.4l3.1-5.4h6.2l3.1,5.4L50.9,64.1z M61.7,58.7l-1.6,2.8h-3.2l-1.6-2.8l1.6-2.8h3.2l0,0h0L61.7,58.7z | |
31 | M60.8,54.8L60.8,54.8L60.8,54.8L59.2,52l1.6-2.8h3.2l1.6,2.8l-1.6,2.8H60.8z"/> | |
32 | </g> | |
33 | </svg> |
Binary diff not shown
Binary diff not shown
0 | <?xml version="1.0" encoding="utf-8"?> | |
1 | <!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> | |
2 | <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | |
3 | viewBox="0 0 144.3 234.4" style="enable-background:new 0 0 144.3 234.4;" xml:space="preserve"> | |
4 | <path d="M143.7,111.3l-4.9-8.4h-9.7l-4.8,8.4l3.4,5.9h-6.8l-3.4,5.9l-6.6-11.5H94.7l-6.6,11.5l-6.6-11.5H68.2l10.7-18.5l-12.1-21 | |
5 | H45.4l9.3-16l6.6,11.5h16.1l8.1-14l-6.6-11.5h21.4l10.7-18.5l3.4,5.9h6.8l-3.4,5.9l4.8,8.4h9.7l4.9-8.4l-4.9-8.4h-6.8l3.4-5.9 | |
6 | l-4.9-8.4h-9.7l-3.4,5.9L100.2,0H76L65.3,18.5L58.6,7.1H42.5l-8.1,14l6.6,11.5H19.7l-12.1,21l12.1,21h21.4l-9.2,16l-6.7-11.6H8.8 | |
7 | L0.6,93.1l8.2,14.1h16.3l6.7-11.6l9.3,16.1h-7.9l-5.4,9.4l5.4,9.4H44l5.4-9.4l-4-6.9h18.5l-6.6,11.5l8.1,14h13.2l-3.4,5.9l4.8,8.4 | |
8 | h9.7l4.9-8.4l-4.9-8.4H83l5.2-8.9l6.6,11.5h16.1l6.6-11.5l3.4,5.9h9.7l4.8-8.4l-3.4-5.9h6.8L143.7,111.3z M45.4,35h13.2l6.6-11.5 | |
9 | l9.3,16H61.3L54.7,51L45.4,35z M76,65H62.7l-6.6-11.5l6.6-11.5H76l6.6,11.5L76,65z M130.8,29.4l3.4,5.9l-3.4,5.9H124l-3.4-5.9 | |
10 | l3.4-5.9h0l0,0H130.8z M115.8,15.1h6.8L126,21l-3.4,5.9h0l0,0h-6.8l-3.4-5.9L115.8,15.1z M77.4,2.5h21.4L109.5,21L98.8,39.6H77.4 | |
11 | L66.7,21L77.4,2.5z M37.3,21L44,9.6h13.2L63.8,21l-6.6,11.5H44h0L37.3,21z M21.1,72.1L10.4,53.5L21.1,35h21.4h0l10.7,18.5L42.5,72.1 | |
12 | h0H21.1z M23.6,104.7H10.2L3.5,93.1l6.7-11.6h13.4l6.7,11.6L23.6,104.7z M43.9,74.6L43.9,74.6h21.4L76,93.1l-10.7,18.5H43.9 | |
13 | L33.3,93.1L43.9,74.6z M42.5,127.8h-7.9l-4-6.8l4-6.8h7.9l4,6.8L42.5,127.8z M60.2,125.6l6.6-11.5h13.2l6.6,11.5l-6.6,11.5H66.8 | |
14 | L60.2,125.6z M91.7,145.4l-3.4,5.9h-6.8l-3.4-5.9l3.4-5.9h6.8L91.7,145.4z M109.4,137.1H96.2l-6.6-11.5l6.6-11.5h13.2l6.6,11.5 | |
15 | L109.4,137.1z M132.6,125.6l-3.4,5.9h-6.8l-3.4-5.9l3.4-5.9h6.8l0,0h0L132.6,125.6z M130.6,117.2L130.6,117.2L130.6,117.2l-3.4-5.9 | |
16 | l3.4-5.9h6.8l3.4,5.9l-3.4,5.9H130.6z"/> | |
17 | <g> | |
18 | <path d="M22.3,197.4h2v-0.9c0-7-3.8-9.4-9.1-9.4c-6,0-10.3,3.5-10.3,3.5l-2.3-3.8c0,0,4.8-4.1,13-4.1c8.8,0,13.6,4.9,13.6,14v23.3 | |
19 | h-4.7v-4c0-1.8,0.1-3,0.1-3h-0.1c0,0-3,7.9-12.2,7.9C6.3,220.8,0,217.2,0,210C0,197.7,16,197.4,22.3,197.4z M13.2,216.6 | |
20 | c7,0,11.2-7.2,11.2-13.5v-1.6h-2.1c-5.7,0-17.2,0.2-17.2,8.3C5.1,213.1,7.7,216.6,13.2,216.6z"/> | |
21 | <path d="M35.3,183.5H40v3.5c0,1.6-0.1,2.8-0.1,2.8H40c0,0,3-7.2,12.6-7.2c9.8,0,15.9,7.8,15.9,19.1c0,11.5-6.9,19-16.3,19 | |
22 | c-8.8,0-11.9-7-11.9-7h-0.1c0,0,0.1,1.3,0.1,3.2v17.4h-4.9V183.5z M51.7,216.4c6.4,0,11.7-5.4,11.7-14.7c0-8.9-4.7-14.6-11.5-14.6 | |
23 | c-6.1,0-11.8,4.3-11.8,14.7C40.1,209.1,44.2,216.4,51.7,216.4z"/> | |
24 | <path d="M73.8,183.5h4.7v3.5c0,1.6-0.1,2.8-0.1,2.8h0.1c0,0,3-7.2,12.6-7.2c9.8,0,15.9,7.8,15.9,19.1c0,11.5-6.9,19-16.3,19 | |
25 | c-8.8,0-11.9-7-11.9-7h-0.1c0,0,0.1,1.3,0.1,3.2v17.4h-4.9V183.5z M90.2,216.4c6.4,0,11.7-5.4,11.7-14.7c0-8.9-4.7-14.6-11.5-14.6 | |
26 | c-6.1,0-11.8,4.3-11.8,14.7C78.6,209.1,82.7,216.4,90.2,216.4z"/> | |
27 | <path d="M130.1,182.6c8.9,0,13.3,5.2,13.3,5.2l-2.5,3.7c0,0-4.1-4.4-10.6-4.4c-8,0-14.2,5.9-14.2,14.6c0,8.5,6.3,14.6,14.3,14.6 | |
28 | c7.4,0,11.7-5.1,11.7-5.1l2.2,3.8c0,0-4.9,5.7-14.2,5.7c-10.8,0-19.1-7.7-19.1-19C111,190.5,119.4,182.6,130.1,182.6z"/> | |
29 | </g> | |
30 | </svg> |
Binary diff not shown
0 | <?xml version="1.0" encoding="utf-8"?> | |
1 | <!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> | |
2 | <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | |
3 | viewBox="0 0 144.3 234.4" style="enable-background:new 0 0 144.3 234.4;" xml:space="preserve"> | |
4 | <path style="fill:#FFB547;" d="M143.7,111.3l-4.9-8.4h-9.7l-4.8,8.4l3.4,5.9h-6.8l-3.4,5.9l-6.6-11.5H94.7l-6.6,11.5l-6.6-11.5H68.2 | |
5 | l10.7-18.5l-12.1-21H45.4l9.3-16l6.6,11.5h16.1l8.1-14l-6.6-11.5h21.4l10.7-18.5l3.4,5.9h6.8l-3.4,5.9l4.8,8.4h9.7l4.9-8.4l-4.9-8.4 | |
6 | h-6.8l3.4-5.9l-4.9-8.4h-9.7l-3.4,5.9L100.2,0H76L65.3,18.5L58.6,7.1H42.5l-8.1,14l6.6,11.5H19.7l-12.1,21l12.1,21h21.4l-9.2,16 | |
7 | l-6.7-11.6H8.8L0.6,93.1l8.2,14.1h16.3l6.7-11.6l9.3,16.1h-7.9l-5.4,9.4l5.4,9.4H44l5.4-9.4l-4-6.9h18.5l-6.6,11.5l8.1,14h13.2 | |
8 | l-3.4,5.9l4.8,8.4h9.7l4.9-8.4l-4.9-8.4H83l5.2-8.9l6.6,11.5h16.1l6.6-11.5l3.4,5.9h9.7l4.8-8.4l-3.4-5.9h6.8L143.7,111.3z M45.4,35 | |
9 | h13.2l6.6-11.5l9.3,16H61.3L54.7,51L45.4,35z M76,65H62.7l-6.6-11.5l6.6-11.5H76l6.6,11.5L76,65z M130.8,29.4l3.4,5.9l-3.4,5.9H124 | |
10 | l-3.4-5.9l3.4-5.9h0l0,0H130.8z M115.8,15.1h6.8L126,21l-3.4,5.9h0l0,0h-6.8l-3.4-5.9L115.8,15.1z M77.4,2.5h21.4L109.5,21 | |
11 | L98.8,39.6H77.4L66.7,21L77.4,2.5z M37.3,21L44,9.6h13.2L63.8,21l-6.6,11.5H44h0L37.3,21z M21.1,72.1L10.4,53.5L21.1,35h21.4h0 | |
12 | l10.7,18.5L42.5,72.1h0H21.1z M23.6,104.7H10.2L3.5,93.1l6.7-11.6h13.4l6.7,11.6L23.6,104.7z M43.9,74.6L43.9,74.6h21.4L76,93.1 | |
13 | l-10.7,18.5H43.9L33.3,93.1L43.9,74.6z M42.5,127.8h-7.9l-4-6.8l4-6.8h7.9l4,6.8L42.5,127.8z M60.2,125.6l6.6-11.5h13.2l6.6,11.5 | |
14 | l-6.6,11.5H66.8L60.2,125.6z M91.7,145.4l-3.4,5.9h-6.8l-3.4-5.9l3.4-5.9h6.8L91.7,145.4z M109.4,137.1H96.2l-6.6-11.5l6.6-11.5 | |
15 | h13.2l6.6,11.5L109.4,137.1z M132.6,125.6l-3.4,5.9h-6.8l-3.4-5.9l3.4-5.9h6.8l0,0h0L132.6,125.6z M130.6,117.2L130.6,117.2 | |
16 | L130.6,117.2l-3.4-5.9l3.4-5.9h6.8l3.4,5.9l-3.4,5.9H130.6z"/> | |
17 | <g> | |
18 | <path style="fill:#F38B00;" d="M22.3,197.4h2v-0.9c0-7-3.8-9.4-9.1-9.4c-6,0-10.3,3.5-10.3,3.5l-2.3-3.8c0,0,4.8-4.1,13-4.1 | |
19 | c8.8,0,13.6,4.9,13.6,14v23.3h-4.7v-4c0-1.8,0.1-3,0.1-3h-0.1c0,0-3,7.9-12.2,7.9C6.3,220.8,0,217.2,0,210 | |
20 | C0,197.7,16,197.4,22.3,197.4z M13.2,216.6c7,0,11.2-7.2,11.2-13.5v-1.6h-2.1c-5.7,0-17.2,0.2-17.2,8.3 | |
21 | C5.1,213.1,7.7,216.6,13.2,216.6z"/> | |
22 | <path style="fill:#F38B00;" d="M35.3,183.5H40v3.5c0,1.6-0.1,2.8-0.1,2.8H40c0,0,3-7.2,12.6-7.2c9.8,0,15.9,7.8,15.9,19.1 | |
23 | c0,11.5-6.9,19-16.3,19c-8.8,0-11.9-7-11.9-7h-0.1c0,0,0.1,1.3,0.1,3.2v17.4h-4.9V183.5z M51.7,216.4c6.4,0,11.7-5.4,11.7-14.7 | |
24 | c0-8.9-4.7-14.6-11.5-14.6c-6.1,0-11.8,4.3-11.8,14.7C40.1,209.1,44.2,216.4,51.7,216.4z"/> | |
25 | <path style="fill:#F38B00;" d="M73.8,183.5h4.7v3.5c0,1.6-0.1,2.8-0.1,2.8h0.1c0,0,3-7.2,12.6-7.2c9.8,0,15.9,7.8,15.9,19.1 | |
26 | c0,11.5-6.9,19-16.3,19c-8.8,0-11.9-7-11.9-7h-0.1c0,0,0.1,1.3,0.1,3.2v17.4h-4.9V183.5z M90.2,216.4c6.4,0,11.7-5.4,11.7-14.7 | |
27 | c0-8.9-4.7-14.6-11.5-14.6c-6.1,0-11.8,4.3-11.8,14.7C78.6,209.1,82.7,216.4,90.2,216.4z"/> | |
28 | <path style="fill:#F38B00;" d="M130.1,182.6c8.9,0,13.3,5.2,13.3,5.2l-2.5,3.7c0,0-4.1-4.4-10.6-4.4c-8,0-14.2,5.9-14.2,14.6 | |
29 | c0,8.5,6.3,14.6,14.3,14.6c7.4,0,11.7-5.1,11.7-5.1l2.2,3.8c0,0-4.9,5.7-14.2,5.7c-10.8,0-19.1-7.7-19.1-19 | |
30 | C111,190.5,119.4,182.6,130.1,182.6z"/> | |
31 | </g> | |
32 | </svg> |
0 | <?xml version="1.0" encoding="utf-8"?> | |
1 | <!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> | |
2 | <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | |
3 | viewBox="0 0 144.3 234.4" style="enable-background:new 0 0 144.3 234.4;" xml:space="preserve"> | |
4 | <g> | |
5 | <path style="fill:#FFFFFF;" d="M143.7,111.3l-4.9-8.4h-9.7l-4.8,8.4l3.4,5.9h-6.8l-3.4,5.9l-6.6-11.5H94.7l-6.6,11.5l-6.6-11.5 | |
6 | H68.2l10.7-18.5l-12.1-21H45.4l9.3-16l6.6,11.5h16.1l8.1-14l-6.6-11.5h21.4l10.7-18.5l3.4,5.9h6.8l-3.4,5.9l4.8,8.4h9.7l4.9-8.4 | |
7 | l-4.9-8.4h-6.8l3.4-5.9l-4.9-8.4h-9.7l-3.4,5.9L100.2,0H76L65.3,18.5L58.6,7.1H42.5l-8.1,14l6.6,11.5H19.7l-12.1,21l12.1,21h21.4 | |
8 | l-9.2,16l-6.7-11.6H8.8L0.6,93.1l8.2,14.1h16.3l6.7-11.6l9.3,16.1h-7.9l-5.4,9.4l5.4,9.4H44l5.4-9.4l-4-6.9h18.5l-6.6,11.5l8.1,14 | |
9 | h13.2l-3.4,5.9l4.8,8.4h9.7l4.9-8.4l-4.9-8.4H83l5.2-8.9l6.6,11.5h16.1l6.6-11.5l3.4,5.9h9.7l4.8-8.4l-3.4-5.9h6.8L143.7,111.3z | |
10 | M45.4,35h13.2l6.6-11.5l9.3,16H61.3L54.7,51L45.4,35z M76,65H62.7l-6.6-11.5l6.6-11.5H76l6.6,11.5L76,65z M130.8,29.4l3.4,5.9 | |
11 | l-3.4,5.9H124l-3.4-5.9l3.4-5.9h0l0,0H130.8z M115.8,15.1h6.8L126,21l-3.4,5.9h0l0,0h-6.8l-3.4-5.9L115.8,15.1z M77.4,2.5h21.4 | |
12 | L109.5,21L98.8,39.6H77.4L66.7,21L77.4,2.5z M37.3,21L44,9.6h13.2L63.8,21l-6.6,11.5H44h0L37.3,21z M21.1,72.1L10.4,53.5L21.1,35 | |
13 | h21.4h0l10.7,18.5L42.5,72.1h0H21.1z M23.6,104.7H10.2L3.5,93.1l6.7-11.6h13.4l6.7,11.6L23.6,104.7z M43.9,74.6L43.9,74.6h21.4 | |
14 | L76,93.1l-10.7,18.5H43.9L33.3,93.1L43.9,74.6z M42.5,127.8h-7.9l-4-6.8l4-6.8h7.9l4,6.8L42.5,127.8z M60.2,125.6l6.6-11.5h13.2 | |
15 | l6.6,11.5l-6.6,11.5H66.8L60.2,125.6z M91.7,145.4l-3.4,5.9h-6.8l-3.4-5.9l3.4-5.9h6.8L91.7,145.4z M109.4,137.1H96.2l-6.6-11.5 | |
16 | l6.6-11.5h13.2l6.6,11.5L109.4,137.1z M132.6,125.6l-3.4,5.9h-6.8l-3.4-5.9l3.4-5.9h6.8l0,0h0L132.6,125.6z M130.6,117.2 | |
17 | L130.6,117.2L130.6,117.2l-3.4-5.9l3.4-5.9h6.8l3.4,5.9l-3.4,5.9H130.6z"/> | |
18 | <g> | |
19 | <path style="fill:#FFFFFF;" d="M22.3,197.4h2v-0.9c0-7-3.8-9.4-9.1-9.4c-6,0-10.3,3.5-10.3,3.5l-2.3-3.8c0,0,4.8-4.1,13-4.1 | |
20 | c8.8,0,13.6,4.9,13.6,14v23.3h-4.7v-4c0-1.8,0.1-3,0.1-3h-0.1c0,0-3,7.9-12.2,7.9C6.3,220.8,0,217.2,0,210 | |
21 | C0,197.7,16,197.4,22.3,197.4z M13.2,216.6c7,0,11.2-7.2,11.2-13.5v-1.6h-2.1c-5.7,0-17.2,0.2-17.2,8.3 | |
22 | C5.1,213.1,7.7,216.6,13.2,216.6z"/> | |
23 | <path style="fill:#FFFFFF;" d="M35.3,183.5H40v3.5c0,1.6-0.1,2.8-0.1,2.8H40c0,0,3-7.2,12.6-7.2c9.8,0,15.9,7.8,15.9,19.1 | |
24 | c0,11.5-6.9,19-16.3,19c-8.8,0-11.9-7-11.9-7h-0.1c0,0,0.1,1.3,0.1,3.2v17.4h-4.9V183.5z M51.7,216.4c6.4,0,11.7-5.4,11.7-14.7 | |
25 | c0-8.9-4.7-14.6-11.5-14.6c-6.1,0-11.8,4.3-11.8,14.7C40.1,209.1,44.2,216.4,51.7,216.4z"/> | |
26 | <path style="fill:#FFFFFF;" d="M73.8,183.5h4.7v3.5c0,1.6-0.1,2.8-0.1,2.8h0.1c0,0,3-7.2,12.6-7.2c9.8,0,15.9,7.8,15.9,19.1 | |
27 | c0,11.5-6.9,19-16.3,19c-8.8,0-11.9-7-11.9-7h-0.1c0,0,0.1,1.3,0.1,3.2v17.4h-4.9V183.5z M90.2,216.4c6.4,0,11.7-5.4,11.7-14.7 | |
28 | c0-8.9-4.7-14.6-11.5-14.6c-6.1,0-11.8,4.3-11.8,14.7C78.6,209.1,82.7,216.4,90.2,216.4z"/> | |
29 | <path style="fill:#FFFFFF;" d="M130.1,182.6c8.9,0,13.3,5.2,13.3,5.2l-2.5,3.7c0,0-4.1-4.4-10.6-4.4c-8,0-14.2,5.9-14.2,14.6 | |
30 | c0,8.5,6.3,14.6,14.3,14.6c7.4,0,11.7-5.1,11.7-5.1l2.2,3.8c0,0-4.9,5.7-14.2,5.7c-10.8,0-19.1-7.7-19.1-19 | |
31 | C111,190.5,119.4,182.6,130.1,182.6z"/> | |
32 | </g> | |
33 | </g> | |
34 | </svg> |
2148 | 2148 | ` |
2149 | 2149 | { |
2150 | 2150 | "acKind": "ImageManifest", |
2151 | "acVersion": "0.6.1", | |
2151 | "acVersion": "0.7.1", | |
2152 | 2152 | "name": "example.com/test_empty_rootfs" |
2153 | 2153 | } |
2154 | 2154 | `, |
2174 | 2174 | ` |
2175 | 2175 | { |
2176 | 2176 | "acKind": "ImageManifest", |
2177 | "acVersion": "0.6.1", | |
2177 | "acVersion": "0.7.1", | |
2178 | 2178 | "name": "example.com/test_empty_rootfs_pwl", |
2179 | 2179 | "pathWhitelist": ["foo"] |
2180 | 2180 | } |
11 | 11 | // See the License for the specific language governing permissions and |
12 | 12 | // limitations under the License. |
13 | 13 | |
14 | // +build linux freebsd netbsd openbsd | |
14 | // +build linux freebsd netbsd openbsd darwin | |
15 | 15 | |
16 | 16 | package device |
17 | 17 |
19 | 19 | imj := ` |
20 | 20 | { |
21 | 21 | "acKind": "ImageManifest", |
22 | "acVersion": "0.6.1", | |
22 | "acVersion": "0.7.1", | |
23 | 23 | "name": "example.com/test" |
24 | 24 | } |
25 | 25 | ` |
0 | // Copyright 2015 The appc Authors | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | // you may not use this file except in compliance with the License. | |
4 | // You may obtain a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, | |
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | // See the License for the specific language governing permissions and | |
12 | // limitations under the License. | |
13 | ||
14 | package lastditch | |
15 | ||
16 | import ( | |
17 | "fmt" | |
18 | "strings" | |
19 | ) | |
20 | ||
21 | // extJ returns a JSON snippet describing an extra field with a given | |
22 | // name | |
23 | func extJ(name string) string { | |
24 | return fmt.Sprintf(`"%s": [],`, name) | |
25 | } | |
26 | ||
27 | // labsJ returns a labels array JSON snippet with given labels | |
28 | func labsJ(labels ...string) string { | |
29 | return fmt.Sprintf("[%s]", strings.Join(labels, ",")) | |
30 | } | |
31 | ||
32 | // labsI returns a labels array instance with given labels | |
33 | func labsI(labels ...Label) Labels { | |
34 | if labels == nil { | |
35 | return Labels{} | |
36 | } | |
37 | return labels | |
38 | } | |
39 | ||
40 | // labJ returns a label JSON snippet with given name and value | |
41 | func labJ(name, value, extra string) string { | |
42 | return fmt.Sprintf(` | |
43 | { | |
44 | %s | |
45 | "name": "%s", | |
46 | "value": "%s" | |
47 | }`, extra, name, value) | |
48 | } | |
49 | ||
50 | // labI returns a label instance with given name and value | |
51 | func labI(name, value string) Label { | |
52 | return Label{ | |
53 | Name: name, | |
54 | Value: value, | |
55 | } | |
56 | } |
0 | // Copyright 2015 The appc Authors | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | // you may not use this file except in compliance with the License. | |
4 | // You may obtain a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, | |
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | // See the License for the specific language governing permissions and | |
12 | // limitations under the License. | |
13 | ||
14 | // Package lastditch provides fallback redefinitions of parts of | |
15 | // schemas provided by schema package. | |
16 | // | |
17 | // Almost no validation of schemas is done (besides checking if data | |
18 | // really is `JSON`-encoded and kind is either `ImageManifest` or | |
19 | // `PodManifest`. This is to get as much data as possible from an | |
20 | // invalid manifest. The main aim of the package is to be used for the | |
21 | // better error reporting. The another aim might be to force some | |
22 | // operation (like removing a pod), which would otherwise fail because | |
23 | // of an invalid manifest. | |
24 | // | |
25 | // To avoid validation during deserialization, types provided by this | |
26 | // package use plain strings. | |
27 | package lastditch |
0 | // Copyright 2015 The appc Authors | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | // you may not use this file except in compliance with the License. | |
4 | // You may obtain a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, | |
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | // See the License for the specific language governing permissions and | |
12 | // limitations under the License. | |
13 | ||
14 | package lastditch | |
15 | ||
16 | import ( | |
17 | "encoding/json" | |
18 | ||
19 | "github.com/appc/spec/schema" | |
20 | "github.com/appc/spec/schema/types" | |
21 | ) | |
22 | ||
23 | type ImageManifest struct { | |
24 | ACVersion string `json:"acVersion"` | |
25 | ACKind string `json:"acKind"` | |
26 | Name string `json:"name"` | |
27 | Labels Labels `json:"labels,omitempty"` | |
28 | } | |
29 | ||
30 | // a type just to avoid a recursion during unmarshalling | |
31 | type imageManifest ImageManifest | |
32 | ||
33 | func (im *ImageManifest) UnmarshalJSON(data []byte) error { | |
34 | i := imageManifest(*im) | |
35 | err := json.Unmarshal(data, &i) | |
36 | if err != nil { | |
37 | return err | |
38 | } | |
39 | if i.ACKind != string(schema.ImageManifestKind) { | |
40 | return types.InvalidACKindError(schema.ImageManifestKind) | |
41 | } | |
42 | *im = ImageManifest(i) | |
43 | return nil | |
44 | } |
0 | // Copyright 2015 The appc Authors | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | // you may not use this file except in compliance with the License. | |
4 | // You may obtain a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, | |
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | // See the License for the specific language governing permissions and | |
12 | // limitations under the License. | |
13 | ||
14 | package lastditch | |
15 | ||
16 | import ( | |
17 | "fmt" | |
18 | "reflect" | |
19 | "testing" | |
20 | ) | |
21 | ||
22 | func TestInvalidImageManifest(t *testing.T) { | |
23 | tests := []struct { | |
24 | desc string | |
25 | json string | |
26 | expected ImageManifest | |
27 | }{ | |
28 | { | |
29 | desc: "Check an empty image manifest", | |
30 | json: imgJ("", labsJ(), ""), | |
31 | expected: imgI("", labsI()), | |
32 | }, | |
33 | { | |
34 | desc: "Check an image manifest with an empty label", | |
35 | json: imgJ("example.com/test!", labsJ(labJ("", "", "")), ""), | |
36 | expected: imgI("example.com/test!", labsI(labI("", ""))), | |
37 | }, | |
38 | { | |
39 | desc: "Check an image manifest with an invalid name", | |
40 | json: imgJ("example.com/test!", labsJ(), ""), | |
41 | expected: imgI("example.com/test!", labsI()), | |
42 | }, | |
43 | { | |
44 | desc: "Check an image manifest with labels with invalid names", | |
45 | json: imgJ("im", labsJ(labJ("!n1", "v1", ""), labJ("N2~", "v2", "")), ""), | |
46 | expected: imgI("im", labsI(labI("!n1", "v1"), labI("N2~", "v2"))), | |
47 | }, | |
48 | { | |
49 | desc: "Check an image manifest with duplicated labels", | |
50 | json: imgJ("im", labsJ(labJ("n1", "v1", ""), labJ("n1", "v2", "")), ""), | |
51 | expected: imgI("im", labsI(labI("n1", "v1"), labI("n1", "v2"))), | |
52 | }, | |
53 | { | |
54 | desc: "Check an image manifest with some extra fields", | |
55 | json: imgJ("im", labsJ(), extJ("stuff")), | |
56 | expected: imgI("im", labsI()), | |
57 | }, | |
58 | { | |
59 | desc: "Check an image manifest with a label containing some extra fields", | |
60 | json: imgJ("im", labsJ(labJ("n1", "v1", extJ("clutter"))), extJ("stuff")), | |
61 | expected: imgI("im", labsI(labI("n1", "v1"))), | |
62 | }, | |
63 | } | |
64 | for _, tt := range tests { | |
65 | got := ImageManifest{} | |
66 | if err := got.UnmarshalJSON([]byte(tt.json)); err != nil { | |
67 | t.Errorf("%s: unexpected error during unmarshalling image manifest: %v", tt.desc, err) | |
68 | } | |
69 | if !reflect.DeepEqual(tt.expected, got) { | |
70 | t.Errorf("%s: did not get expected image manifest, got:\n %#v\nexpected:\n %#v", tt.desc, got, tt.expected) | |
71 | } | |
72 | } | |
73 | } | |
74 | ||
75 | func TestBogusImageManifest(t *testing.T) { | |
76 | bogus := []string{` | |
77 | { | |
78 | "acKind": "Bogus", | |
79 | "acVersion": "0.7.1", | |
80 | } | |
81 | `, ` | |
82 | <html> | |
83 | <head> | |
84 | <title>Certainly not a JSON</title> | |
85 | </head> | |
86 | </html>`, | |
87 | } | |
88 | ||
89 | for _, str := range bogus { | |
90 | im := ImageManifest{} | |
91 | if im.UnmarshalJSON([]byte(str)) == nil { | |
92 | t.Errorf("bogus image manifest unmarshalled successfully: %s", str) | |
93 | } | |
94 | } | |
95 | } | |
96 | ||
97 | // imgJ returns an image manifest JSON with given name and labels | |
98 | func imgJ(name, labels, extra string) string { | |
99 | return fmt.Sprintf(` | |
100 | { | |
101 | %s | |
102 | "acKind": "ImageManifest", | |
103 | "acVersion": "0.7.1", | |
104 | "name": "%s", | |
105 | "labels": %s | |
106 | }`, extra, name, labels) | |
107 | } | |
108 | ||
109 | // imgI returns an image manifest instance with given name and labels | |
110 | func imgI(name string, labels Labels) ImageManifest { | |
111 | return ImageManifest{ | |
112 | ACVersion: "0.7.1", | |
113 | ACKind: "ImageManifest", | |
114 | Name: name, | |
115 | Labels: labels, | |
116 | } | |
117 | } |
0 | // Copyright 2015 The appc Authors | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | // you may not use this file except in compliance with the License. | |
4 | // You may obtain a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, | |
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | // See the License for the specific language governing permissions and | |
12 | // limitations under the License. | |
13 | ||
14 | package lastditch | |
15 | ||
16 | import ( | |
17 | "encoding/json" | |
18 | ) | |
19 | ||
20 | type Labels []Label | |
21 | ||
22 | // a type just to avoid a recursion during unmarshalling | |
23 | type labels Labels | |
24 | ||
25 | type Label struct { | |
26 | Name string `json:"name"` | |
27 | Value string `json:"value"` | |
28 | } | |
29 | ||
30 | func (l *Labels) UnmarshalJSON(data []byte) error { | |
31 | var jl labels | |
32 | if err := json.Unmarshal(data, &jl); err != nil { | |
33 | return err | |
34 | } | |
35 | *l = Labels(jl) | |
36 | return nil | |
37 | } |
0 | // Copyright 2015 The appc Authors | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | // you may not use this file except in compliance with the License. | |
4 | // You may obtain a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, | |
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | // See the License for the specific language governing permissions and | |
12 | // limitations under the License. | |
13 | ||
14 | package lastditch | |
15 | ||
16 | import ( | |
17 | "encoding/json" | |
18 | ||
19 | "github.com/appc/spec/schema" | |
20 | "github.com/appc/spec/schema/types" | |
21 | ) | |
22 | ||
23 | type PodManifest struct { | |
24 | ACVersion string `json:"acVersion"` | |
25 | ACKind string `json:"acKind"` | |
26 | Apps AppList `json:"apps"` | |
27 | } | |
28 | ||
29 | type AppList []RuntimeApp | |
30 | ||
31 | type RuntimeApp struct { | |
32 | Name string `json:"name"` | |
33 | Image RuntimeImage `json:"image"` | |
34 | } | |
35 | ||
36 | type RuntimeImage struct { | |
37 | Name string `json:"name"` | |
38 | ID string `json:"id"` | |
39 | Labels Labels `json:"labels,omitempty"` | |
40 | } | |
41 | ||
42 | // a type just to avoid a recursion during unmarshalling | |
43 | type podManifest PodManifest | |
44 | ||
45 | func (pm *PodManifest) UnmarshalJSON(data []byte) error { | |
46 | p := podManifest(*pm) | |
47 | err := json.Unmarshal(data, &p) | |
48 | if err != nil { | |
49 | return err | |
50 | } | |
51 | if p.ACKind != string(schema.PodManifestKind) { | |
52 | return types.InvalidACKindError(schema.PodManifestKind) | |
53 | } | |
54 | *pm = PodManifest(p) | |
55 | return nil | |
56 | } |
0 | // Copyright 2015 The appc Authors | |
1 | // | |
2 | // Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | // you may not use this file except in compliance with the License. | |
4 | // You may obtain a copy of the License at | |
5 | // | |
6 | // http://www.apache.org/licenses/LICENSE-2.0 | |
7 | // | |
8 | // Unless required by applicable law or agreed to in writing, software | |
9 | // distributed under the License is distributed on an "AS IS" BASIS, | |
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | // See the License for the specific language governing permissions and | |
12 | // limitations under the License. | |
13 | ||
14 | package lastditch | |
15 | ||
16 | import ( | |
17 | "fmt" | |
18 | "reflect" | |
19 | "strings" | |
20 | "testing" | |
21 | ) | |
22 | ||
23 | func TestInvalidPodManifest(t *testing.T) { | |
24 | tests := []struct { | |
25 | desc string | |
26 | json string | |
27 | expected PodManifest | |
28 | }{ | |
29 | { | |
30 | desc: "Check an empty pod manifest", | |
31 | json: podJ(appsJ(), ""), | |
32 | expected: podI(appsI()), | |
33 | }, | |
34 | { | |
35 | desc: "Check a pod manifest with an empty app", | |
36 | json: podJ(appsJ(appJ("", rImgJ("i", "id", labsJ(), ""), "")), ""), | |
37 | expected: podI(appsI(appI("", rImgI("i", "id", labsI())))), | |
38 | }, | |
39 | { | |
40 | desc: "Check a pod manifest with an app based on an empty image", | |
41 | json: podJ(appsJ(appJ("a", rImgJ("", "", labsJ(), ""), "")), ""), | |
42 | expected: podI(appsI(appI("a", rImgI("", "", labsI())))), | |
43 | }, | |
44 | { | |
45 | desc: "Check a pod manifest with an app based on an image containing an empty label", | |
46 | json: podJ(appsJ(appJ("a", rImgJ("", "", labsJ(labJ("", "", "")), ""), "")), ""), | |
47 | expected: podI(appsI(appI("a", rImgI("", "", labsI(labI("", "")))))), | |
48 | }, | |
49 | { | |
50 | desc: "Check a pod manifest with an invalid app name", | |
51 | json: podJ(appsJ(appJ("!", rImgJ("i", "id", labsJ(), ""), "")), ""), | |
52 | expected: podI(appsI(appI("!", rImgI("i", "id", labsI())))), | |
53 | }, | |
54 | { | |
55 | desc: "Check a pod manifest with duplicated app names", | |
56 | json: podJ(appsJ(appJ("a", rImgJ("i", "id", labsJ(), ""), ""), appJ("a", rImgJ("", "", labsJ(), ""), "")), ""), | |
57 | expected: podI(appsI(appI("a", rImgI("i", "id", labsI())), appI("a", rImgI("", "", labsI())))), | |
58 | }, | |
59 | { | |
60 | desc: "Check a pod manifest with an invalid image name and ID", | |
61 | json: podJ(appsJ(appJ("?", rImgJ("!!!", "&&&", labsJ(), ""), "")), ""), | |
62 | expected: podI(appsI(appI("?", rImgI("!!!", "&&&", labsI())))), | |
63 | }, | |
64 | { | |
65 | desc: "Check a pod manifest with an app based on an image containing labels with invalid names", | |
66 | json: podJ(appsJ(appJ("a", rImgJ("i", "id", labsJ(labJ("!n1", "v1", ""), labJ("N2~", "v2", "")), ""), "")), ""), | |
67 | expected: podI(appsI(appI("a", rImgI("i", "id", labsI(labI("!n1", "v1"), labI("N2~", "v2")))))), | |
68 | }, | |
69 | { | |
70 | desc: "Check a pod manifest with an app based on an image containing repeated labels", | |
71 | json: podJ(appsJ(appJ("a", rImgJ("i", "id", labsJ(labJ("n1", "v1", ""), labJ("n1", "v2", "")), ""), "")), ""), | |
72 | expected: podI(appsI(appI("a", rImgI("i", "id", labsI(labI("n1", "v1"), labI("n1", "v2")))))), | |
73 | }, | |
74 | { | |
75 | desc: "Check a pod manifest with some extra fields", | |
76 | json: podJ(appsJ(), extJ("goblins")), | |
77 | expected: podI(appsI()), | |
78 | }, | |
79 | { | |
80 | desc: "Check a pod manifest with an app containing some extra fields", | |
81 | json: podJ(appsJ(appJ("a", rImgJ("i", "id", labsJ(), ""), extJ("trolls"))), extJ("goblins")), | |
82 | expected: podI(appsI(appI("a", rImgI("i", "id", labsI())))), | |
83 | }, | |
84 | { | |
85 | desc: "Check a pod manifest with an app based on an image containing some extra fields", | |
86 | json: podJ(appsJ(appJ("a", rImgJ("i", "id", labsJ(), extJ("stuff")), extJ("trolls"))), extJ("goblins")), | |
87 | expected: podI(appsI(appI("a", rImgI("i", "id", labsI())))), | |
88 | }, | |
89 | { | |
90 | desc: "Check a pod manifest with an app based on an image containing labels with some extra fields", | |
91 | json: podJ(appsJ(appJ("a", rImgJ("i", "id", labsJ(labJ("n", "v", extJ("color"))), extJ("stuff")), extJ("trolls"))), extJ("goblins")), | |
92 | expected: podI(appsI(appI("a", rImgI("i", "id", labsI(labI("n", "v")))))), | |
93 | }, | |
94 | } | |
95 | for _, tt := range tests { | |
96 | got := PodManifest{} | |
97 | if err := got.UnmarshalJSON([]byte(tt.json)); err != nil { | |
98 | t.Errorf("%s: unexpected error during unmarshalling pod manifest: %v", tt.desc, err) | |
99 | } | |
100 | if !reflect.DeepEqual(tt.expected, got) { | |
101 | t.Errorf("%s: did not get expected pod manifest, got:\n %#v\nexpected:\n %#v", tt.desc, got, tt.expected) | |
102 | } | |
103 | } | |
104 | } | |
105 | ||
106 | func TestBogusPodManifest(t *testing.T) { | |
107 | bogus := []string{ | |
108 | ` | |
109 | { | |
110 | "acKind": "Bogus", | |
111 | "acVersion": "0.7.1", | |
112 | } | |
113 | `, | |
114 | ` | |
115 | <html> | |
116 | <head> | |
117 | <title>Certainly not a JSON</title> | |
118 | </head> | |
119 | </html>`, | |
120 | } | |
121 | ||
122 | for _, str := range bogus { | |
123 | pm := PodManifest{} | |
124 | if pm.UnmarshalJSON([]byte(str)) == nil { | |
125 | t.Errorf("bogus pod manifest unmarshalled successfully: %s", str) | |
126 | } | |
127 | } | |
128 | } | |
129 | ||
130 | // podJ returns a pod manifest JSON with given apps | |
131 | func podJ(apps, extra string) string { | |
132 | return fmt.Sprintf(` | |
133 | { | |
134 | %s | |
135 | "acKind": "PodManifest", | |
136 | "acVersion": "0.7.1", | |
137 | "apps": %s | |
138 | }`, extra, apps) | |
139 | } | |
140 | ||
141 | // podI returns a pod manifest instance with given apps | |
142 | func podI(apps AppList) PodManifest { | |
143 | return PodManifest{ | |
144 | ACVersion: "0.7.1", | |
145 | ACKind: "PodManifest", | |
146 | Apps: apps, | |
147 | } | |
148 | } | |
149 | ||
150 | // appsJ returns an applist JSON snippet with given apps | |
151 | func appsJ(apps ...string) string { | |
152 | return fmt.Sprintf("[%s]", strings.Join(apps, ",")) | |
153 | } | |
154 | ||
155 | // appsI returns an applist instance with given apps | |
156 | func appsI(apps ...RuntimeApp) AppList { | |
157 | if apps == nil { | |
158 | return AppList{} | |
159 | } | |
160 | return apps | |
161 | } | |
162 | ||
163 | // appJ returns an app JSON snippet with given name and image | |
164 | func appJ(name, image, extra string) string { | |
165 | return fmt.Sprintf(` | |
166 | { | |
167 | %s | |
168 | "name": "%s", | |
169 | "image": %s | |
170 | }`, extra, name, image) | |
171 | } | |
172 | ||
173 | // appI returns an app instance with given name and image | |
174 | func appI(name string, image RuntimeImage) RuntimeApp { | |
175 | return RuntimeApp{ | |
176 | Name: name, | |
177 | Image: image, | |
178 | } | |
179 | } | |
180 | ||
181 | // rImgJ returns a runtime image JSON snippet with given name, id and | |
182 | // labels | |
183 | func rImgJ(name, id, labels, extra string) string { | |
184 | return fmt.Sprintf(` | |
185 | { | |
186 | %s | |
187 | "name": "%s", | |
188 | "id": "%s", | |
189 | "labels": %s | |
190 | }`, extra, name, id, labels) | |
191 | } | |
192 | ||
193 | // rImgI returns a runtime image instance with given name, id and | |
194 | // labels | |
195 | func rImgI(name, id string, labels Labels) RuntimeImage { | |
196 | return RuntimeImage{ | |
197 | Name: name, | |
198 | ID: id, | |
199 | Labels: labels, | |
200 | } | |
201 | } |
125 | 125 | return nil |
126 | 126 | } |
127 | 127 | |
128 | // Mount describes the mapping between a volume and an apps | |
129 | // MountPoint that will be fulfilled at runtime. | |
128 | // Mount describes the mapping between a volume and the path it is mounted | |
129 | // inside of an app's filesystem. | |
130 | 130 | type Mount struct { |
131 | Volume types.ACName `json:"volume"` | |
132 | MountPoint types.ACName `json:"mountPoint"` | |
131 | Volume types.ACName `json:"volume"` | |
132 | Path string `json:"path"` | |
133 | 133 | } |
134 | 134 | |
135 | 135 | func (r Mount) assertValid() error { |
136 | 136 | if r.Volume.Empty() { |
137 | 137 | return errors.New("volume must be set") |
138 | 138 | } |
139 | if r.MountPoint.Empty() { | |
140 | return errors.New("mountPoint must be set") | |
139 | if r.Path == "" { | |
140 | return errors.New("path must be set") | |
141 | 141 | } |
142 | 142 | return nil |
143 | 143 | } |
21 | 21 | ) |
22 | 22 | |
23 | 23 | type App struct { |
24 | Exec Exec `json:"exec"` | |
25 | EventHandlers []EventHandler `json:"eventHandlers,omitempty"` | |
26 | User string `json:"user"` | |
27 | Group string `json:"group"` | |
28 | WorkingDirectory string `json:"workingDirectory,omitempty"` | |
29 | Environment Environment `json:"environment,omitempty"` | |
30 | MountPoints []MountPoint `json:"mountPoints,omitempty"` | |
31 | Ports []Port `json:"ports,omitempty"` | |
32 | Isolators Isolators `json:"isolators,omitempty"` | |
24 | Exec Exec `json:"exec"` | |
25 | EventHandlers []EventHandler `json:"eventHandlers,omitempty"` | |
26 | User string `json:"user"` | |
27 | Group string `json:"group"` | |
28 | SupplementaryGIDs []int `json:"supplementaryGIDs,omitempty"` | |
29 | WorkingDirectory string `json:"workingDirectory,omitempty"` | |
30 | Environment Environment `json:"environment,omitempty"` | |
31 | MountPoints []MountPoint `json:"mountPoints,omitempty"` | |
32 | Ports []Port `json:"ports,omitempty"` | |
33 | Isolators Isolators `json:"isolators,omitempty"` | |
33 | 34 | } |
34 | 35 | |
35 | 36 | // app is a model to facilitate extra validation during the |
17 | 17 | "encoding/json" |
18 | 18 | "errors" |
19 | 19 | |
20 | "github.com/appc/spec/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" | |
20 | "github.com/appc/spec/Godeps/_workspace/src/k8s.io/kubernetes/pkg/api/resource" | |
21 | 21 | ) |
22 | 22 | |
23 | 23 | var ( |
97 | 97 | { |
98 | 98 | `{ |
99 | 99 | "name": "resource/cpu", |
100 | "value": {"request": "30", "limit": "1"} | |
100 | "value": {"request": "30m", "limit": "1m"} | |
101 | 101 | }`, |
102 | 102 | false, |
103 | 103 | }, |
152 | 152 | [ |
153 | 153 | { |
154 | 154 | "name": "resource/cpu", |
155 | "value": {"request": "30", "limit": "1"} | |
155 | "value": {"request": "30m", "limit": "1m"} | |
156 | 156 | }, |
157 | 157 | { |
158 | 158 | "name": "resource/memory", |
201 | 201 | var r Resource = v |
202 | 202 | glimit := r.Limit() |
203 | 203 | grequest := r.Request() |
204 | if glimit.Value() != tt.wlimit || grequest.Value() != tt.wrequest { | |
205 | t.Errorf("#%d: glimit=%v, want %v, grequest=%v, want %v", i, glimit.Value(), tt.wlimit, grequest.Value(), tt.wrequest) | |
206 | } | |
204 | ||
205 | var vlimit, vrequest int64 | |
206 | if tt.name == "resource/cpu" { | |
207 | vlimit, vrequest = glimit.MilliValue(), grequest.MilliValue() | |
208 | } else { | |
209 | vlimit, vrequest = glimit.Value(), grequest.Value() | |
210 | } | |
211 | ||
212 | if vlimit != tt.wlimit { | |
213 | t.Errorf("#%d: glimit=%v, want %v", i, vlimit, tt.wlimit) | |
214 | } | |
215 | if vrequest != tt.wrequest { | |
216 | t.Errorf("#%d: grequest=%v, want %v", i, vrequest, tt.wrequest) | |
217 | } | |
218 | ||
207 | 219 | case LinuxCapabilitiesSet: |
208 | 220 | var s LinuxCapabilitiesSet = v |
209 | 221 | if !reflect.DeepEqual(s.Set(), tt.wset) { |
34 | 34 | Value string `json:"value"` |
35 | 35 | } |
36 | 36 | |
37 | func (l Labels) assertValid() error { | |
38 | seen := map[ACIdentifier]string{} | |
39 | for _, lbl := range l { | |
40 | if lbl.Name == "name" { | |
41 | return fmt.Errorf(`invalid label name: "name"`) | |
42 | } | |
43 | _, ok := seen[lbl.Name] | |
44 | if ok { | |
45 | return fmt.Errorf(`duplicate labels of name %q`, lbl.Name) | |
46 | } | |
47 | seen[lbl.Name] = lbl.Value | |
48 | } | |
49 | if os, ok := seen["os"]; ok { | |
50 | if validArchs, ok := ValidOSArch[os]; !ok { | |
37 | // IsValidOsArch checks if a OS-architecture combination is valid given a map | |
38 | // of valid OS-architectures | |
39 | func IsValidOSArch(labels map[ACIdentifier]string, validOSArch map[string][]string) error { | |
40 | if os, ok := labels["os"]; ok { | |
41 | if validArchs, ok := validOSArch[os]; !ok { | |
51 | 42 | // Not a whitelisted OS. TODO: how to warn rather than fail? |
52 | validOses := make([]string, 0, len(ValidOSArch)) | |
53 | for validOs := range ValidOSArch { | |
43 | validOses := make([]string, 0, len(validOSArch)) | |
44 | for validOs := range validOSArch { | |
54 | 45 | validOses = append(validOses, validOs) |
55 | 46 | } |
56 | 47 | sort.Strings(validOses) |
58 | 49 | } else { |
59 | 50 | // Whitelisted OS. We check arch here, as arch makes sense only |
60 | 51 | // when os is defined. |
61 | if arch, ok := seen["arch"]; ok { | |
52 | if arch, ok := labels["arch"]; ok { | |
62 | 53 | found := false |
63 | 54 | for _, validArch := range validArchs { |
64 | 55 | if arch == validArch { |
73 | 64 | } |
74 | 65 | } |
75 | 66 | return nil |
67 | } | |
68 | ||
69 | func (l Labels) assertValid() error { | |
70 | seen := map[ACIdentifier]string{} | |
71 | for _, lbl := range l { | |
72 | if lbl.Name == "name" { | |
73 | return fmt.Errorf(`invalid label name: "name"`) | |
74 | } | |
75 | _, ok := seen[lbl.Name] | |
76 | if ok { | |
77 | return fmt.Errorf(`duplicate labels of name %q`, lbl.Name) | |
78 | } | |
79 | seen[lbl.Name] = lbl.Value | |
80 | } | |
81 | return IsValidOSArch(seen, ValidOSArch) | |
76 | 82 | } |
77 | 83 | |
78 | 84 | func (l Labels) MarshalJSON() ([]byte, error) { |
43 | 43 | return &v, nil |
44 | 44 | } |
45 | 45 | |
46 | func (sv SemVer) LessThanMajor(versionB SemVer) bool { | |
47 | majorA := semver.Version(sv).Major | |
48 | majorB := semver.Version(versionB).Major | |
49 | if majorA < majorB { | |
50 | return true | |
51 | } | |
52 | return false | |
53 | } | |
54 | ||
55 | func (sv SemVer) LessThanExact(versionB SemVer) bool { | |
56 | vA := semver.Version(sv) | |
57 | vB := semver.Version(versionB) | |
58 | return vA.LessThan(vB) | |
59 | } | |
60 | ||
46 | 61 | func (sv SemVer) String() string { |
47 | 62 | s := semver.Version(sv) |
48 | 63 | return s.String() |
21 | 21 | // version represents the canonical version of the appc spec and tooling. |
22 | 22 | // For now, the schema and tooling is coupled with the spec itself, so |
23 | 23 | // this must be kept in sync with the VERSION file in the root of the repo. |
24 | version string = "0.6.1" | |
24 | version string = "0.7.1" | |
25 | 25 | ) |
26 | 26 | |
27 | 27 | var ( |
25 | 25 | Each app in a pod will start chrooted into its own unique read-write filesystem before execution. |
26 | 26 | |
27 | 27 | An app's filesystem must be *rendered* in an empty directory by the following process (or equivalent): |
28 | - The `rootfs` contained in the ACI is extracted | |
29 | - If the ACI contains a non-empty `dependencies` field in its `ImageManifest`, the `rootfs` of each dependent image is extracted, in the order in which they are listed | |
30 | - If the ACI contains a non-empty `pathWhitelist` field in its `ImageManifest`, *all* paths not in the whitelist must be removed | |
28 | ||
29 | 1. If the ACI contains a non-empty `dependencies` field in its `ImageManifest`, the `rootfs` of each dependent image is extracted into the target directory, in the order in which they are listed. | |
30 | 2. The `rootfs` contained in the ACI is extracted into the target directory | |
31 | 3. If the ACI contains a non-empty `pathWhitelist` field in its `ImageManifest`, *all* paths not in the whitelist must be removed from the target directory | |
32 | ||
33 | If during rootfs extraction a path is already present in the target directory from an earlier dependency, the previously extracted path MUST be overwritten. If the existing path is a symbolic link to a directory, the link MUST NOT be followed and it MUST be removed and replaced with the new path. | |
31 | 34 | |
32 | 35 | Every execution of an app MUST start from a clean copy of this rendered filesystem. |
33 | 36 | |
48 | 51 | For example, say that the worker-backup and reduce-worker both have a `mountPoint` named "work". |
49 | 52 | In this case, the executor will bind mount the host's `/opt/tenant1/work` directory into the `path` of each of the matching "work" `mountPoint`s of the two app filesystems. |
50 | 53 | |
54 | If the target `path` does not exist in the rendered filesystem, it SHOULD be created, including any missing parent directories. | |
55 | ||
56 | If the target `path` is a non-empty directory, its contents SHOULD be discarded (e.g. obscured by the bind mount). | |
57 | If the target `path` refers to a file, the ACE SHOULD remove that file and create a directory in its place. | |
58 | In either of these cases, the ACE SHOULD warn the user that existing files are being masked by a volume. | |
59 | ||
60 | If multiple targets have overlapping target `path`s (for example, if one is nested within another), the ACE SHOULD consider this an error. | |
61 | ||
62 | The target `path` directories that the ACE creates SHOULD be owned by UID 0 and GID 0, and have access mode `0755` (`rwxr-xr-xr-x`). | |
63 | The ACE implementation MAY provide a method for administrator to specify different permissions on a per-pod basis. | |
64 | ||
65 | The ACE SHOULD NOT create any paths in the host file system, and MUST consider missing volume source paths an error. | |
66 | If the ACE does modify the host file system, it SHOULD be possible to disable this behaviour. | |
67 | The ACE MAY implement single-file volumes if the underlying operating system supports it. | |
68 | ||
69 | If the host volume's `source` path is a symbolic link, the ACE SHOULD consider it an error, and SHOULD NOT attempt to use this link's target as volume. | |
70 | The ACE SHOULD also consider it an error if any intermediate directory in volume's `source` path is a symbolic link. | |
71 | If the ACE chooses to support symbolic links as volume sources, it SHOULD provide a way to enable or disable this behaviour on a per-pod basis (e.g. as a boolean isolator or a command line switch). | |
72 | ||
51 | 73 | #### Network Setup |
52 | 74 | |
53 | 75 | A Pod must have a loopback network interface and zero or more [layer 3](http://en.wikipedia.org/wiki/Network_layer) (commonly called the IP layer) network interfaces, which can be instantiated in any number of ways (e.g. veth, macvlan, ipvlan, device pass-through). |
143 | 165 | Limit and request quantities must always be represented internally (i.e. for encoding and any processing) as an integer value (i.e. NOT floating point) in a resource type's natural base units (e.g., bytes, not megabytes or gigabytes). |
144 | 166 | For convenience, when specified by users quantities may either be unsuffixed, have metric suffices (E, P, T, G, M, K) or binary (power-of-two) suffices (Ei, Pi, Ti, Gi, Mi, Ki). |
145 | 167 | For example, the following strings represent the same value: "128974848", "125952Ki", "123Mi". |
146 | Sub-units (e.g. decimals, "0.3", or milli-units, "300m") are NOT permissible. | |
168 | Small quantities can be represented directly as decimals (e.g., 0.3), or using milli-units (e.g., "300m"). | |
147 | 169 | |
148 | 170 | #### resource/block-bandwidth |
149 | 171 | |
185 | 207 | |
186 | 208 | **Parameters:** |
187 | 209 | |
188 | * **request** milli-cores that are requested | |
189 | * **limit** milli-cores that can be consumed before the kernel temporarily throttles the process | |
210 | * **request** cores that are requested | |
211 | * **limit** cores that can be consumed before the kernel temporarily throttles the process | |
190 | 212 | |
191 | 213 | ```json |
192 | 214 | "name": "resource/cpu", |
193 | 215 | "value": { |
194 | "request": "250", | |
195 | "limit": "500" | |
196 | } | |
197 | ``` | |
198 | ||
199 | **Note**: a milli-core is the milli-seconds/second that the app/pod will be able to run. e.g. 1000 would represent full use of a single CPU core every second. | |
216 | "request": "250m", | |
217 | "limit": "500m" | |
218 | } | |
219 | ``` | |
220 | ||
221 | **Note**: a core is the seconds/second that the app/pod will be able to run. e.g. 1 (or 1000m for 1000 milli-seconds) would represent full use of a single CPU core every second. | |
200 | 222 | |
201 | 223 | #### resource/memory |
202 | 224 |
77 | 77 | ```json |
78 | 78 | { |
79 | 79 | "acKind": "ImageManifest", |
80 | "acVersion": "0.6.1", | |
80 | "acVersion": "0.7.1", | |
81 | 81 | "name": "example.com/reduce-worker", |
82 | 82 | "labels": [ |
83 | 83 | { |
100 | 100 | ], |
101 | 101 | "user": "100", |
102 | 102 | "group": "300", |
103 | "supplementaryGids": [ | |
104 | 400, | |
105 | 500 | |
106 | ], | |
103 | 107 | "eventHandlers": [ |
104 | 108 | { |
105 | 109 | "exec": [ |
126 | 130 | { |
127 | 131 | "name": "resource/cpu", |
128 | 132 | "value": { |
129 | "request": "250", | |
130 | "limit": "500" | |
133 | "request": "250m", | |
134 | "limit": "500m" | |
131 | 135 | } |
132 | 136 | }, |
133 | 137 | { |
220 | 224 | * **app** (object, optional) if present, defines the default parameters that can be used to execute this image as an application. |
221 | 225 | * **exec** (list of strings, required) executable to launch and any flags (must be non-empty; the executable must be an absolute path within the app rootfs; ACE can append or override). These strings are not evaluated in any way and environment variables are not substituted. |
222 | 226 | * **user**, **group** (string, required) indicates either the username/group name or the UID/GID the app is to be run as (freeform string). The user and group values may be all numbers to indicate a UID/GID, however it is possible on some systems (POSIX) to have usernames that are all numerical. The user and group values will first be resolved using the image's own `/etc/passwd` or `/etc/group`. If no valid matches are found, then if the string is all numerical, it shall be converted to an integer and used as the UID/GID. If the user or group field begins with a "/", the owner and group of the file found at that absolute path inside the rootfs is used as the UID/GID of the process. Example values for the fields include `root`, `1000`, or `/usr/bin/ping`. |
227 | * **supplementaryGIDs** (list of unsigned integers, optional) indicates additional (supplementary) group IDs (GIDs) as which the app's processes should run. | |
223 | 228 | * **eventHandlers** (list of objects, optional) allows the app to have several hooks based on lifecycle events. For example, you may want to execute a script before the main process starts up to download a dataset or backup onto the filesystem. An eventHandler is a simple object with two fields - an **exec** (array of strings, ACE can append or override), and a **name** (there may be only one eventHandler of a given name), which must be one of: |
224 | 229 | * **pre-start** - executed and must exit before the long running main **exec** binary is launched |
225 | 230 | * **post-stop** - executed if the main **exec** process is killed. This can be used to cleanup resources in the case of clean application shutdown, but cannot be relied upon in the face of machine failure. |
226 | 231 | * **workingDirectory** (string, optional) working directory of the launched application, relative to the application image's root (must be an absolute path, defaults to "/", ACE can override). If the directory does not exist in the application's assembled rootfs (including any dependent images and mounted volumes), the ACE must fail execution. |
227 | 232 | * **environment** (list of objects, optional) represents the app's environment variables (ACE can append). The listed objects must have two key-value pairs: **name** and **value**. The **name** must consist solely of letters, digits, and underscores '_' as outlined in [IEEE Std 1003.1-2001](http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html). The **value** is an arbitrary string. These values are not evaluated in any way, and no substitutions are made. |
228 | 233 | * **isolators** (list of objects of type [Isolator](types.md#isolator-type), optional) list of isolation steps that SHOULD be applied to the app. |
229 | * **mountPoints** (list of objects, optional) locations where an app is expecting external data to be mounted. The listed objects contain the following key-value pairs: the **name** indicates an executor-defined label to look up a mount point, and the **path** stipulates where it is to be mounted inside the rootfs. The name is restricted to the [AC Name](types.md#ac-name-type) Type formatting. **readOnly** is a boolean indicating whether or not the mount point will be read-only (defaults to "false" if unsupplied). | |
234 | * **mountPoints** (list of objects, optional) locations where an app is expecting external data to be mounted. The listed objects contain the following key-value pairs: the **name** indicates a label to refer to a mount point (which may be used by the executor when resolving a mount to a volume in the PodManifest), and the **path** stipulates where it is to be mounted inside the rootfs. The name is restricted to the [AC Name](types.md#ac-name-type) Type formatting. **readOnly** is a boolean indicating whether or not the mount point will be read-only (defaults to "false" if unsupplied). | |
230 | 235 | * **ports** (list of objects, optional) ports that this app will be listening on once started. This field is informational: example uses include helping users to discover the listening ports of the application, or indicating to executors ports that should be exposed on the host. This information could also optionally be used to limit the inbound connections to the container via firewall rules to only ports that are explicitly exposed. Each object can represent either a single port or a port range (contiguous set of ports). |
231 | 236 | * **name** (string, required, restricted to the [AC Name](#ac-name-type) formatting) descriptive name for this port; for example, "http" or "database". This field is used as a key in the [Pod Manifest](#pod-manifest-schema) when specifying ports to be forwarded from the host. |
232 | 237 | * **protocol** (string, required) protocol that will be used on this port. MAY be any value, but typically SHOULD be a transport layer (Layer 4) protocol - for example, "tcp" or "udp". The executor MAY refuse to execute if this field contains an unrecognized value. |
237 | 242 | * **imageName** (string of type [AC Identifier](types.md#ac-identifier-type), required) name of the dependent App Container Image. |
238 | 243 | * **imageID** (string of type [Image ID](types.md#image-id-type), optional) content hash of the dependency. If provided, the retrieved dependency must match the hash. This can be used to produce deterministic, repeatable builds of an App Container Image that has dependencies. |
239 | 244 | * **labels** (list of objects, optional) a list of the very same form as the aforementioned label objects in the top level ImageManifest. See [Dependency Matching](#dependency-matching) for how these are used. |
245 | * **size** (integer, optional) the size of the image referenced dependency, in bytes. This field is optional; if it is present, the ACE SHOULD ensure it matches when retrieving a dependency, to mitigate "endless data" attacks. | |
240 | 246 | * **pathWhitelist** (list of strings, optional) whitelist of absolute paths that will exist in the app's rootfs after rendering. This must be a complete and absolute set. An empty list is equivalent to an absent value and means that all files in this image and any dependencies will be available in the rootfs. |
241 | * **size** (integer, optional) the size of the image referenced dependency, in bytes. This field is optional; if it is present, the ACE SHOULD ensure it matches when retrieving a dependency, to mitigate "endless data" attacks. | |
242 | 247 | * **annotations** (list of objects, optional) any extra metadata you wish to add to the image. Each object has two key-value pairs: the *name* is restricted to the [AC Identifier](types.md#ac-identifier-type) formatting and *value* is an arbitrary string. Annotation names must be unique within the list. Annotations can be used by systems outside of the ACE (ACE can override). If you are defining new annotations, please consider submitting them to the specification. If you intend for your field to remain special to your application please be a good citizen and prefix an appropriate namespace to your key names. Recognized annotations include: |
243 | 248 | * **created** date on which the image was built (string, [timestamps type](types.md#timestamps-type)) |
244 | 249 | * **authors** contact details of the people or organization responsible for the image (freeform string) |
248 | 253 | #### Dependency Matching |
249 | 254 | |
250 | 255 | Dependency matching is based on a combination of the three different fields of the dependency - **imageName**, **imageID**, and **labels**. |
251 | First, the image discovery mechanism is used to locate a dependency based on the **imageName**. | |
252 | If any labels are specified in the dependency, they are passed to the image discovery mechanism, and will be used when locating the image. | |
253 | ||
254 | If the image discovery process successfully returns an image, it will be compared as follows. | |
255 | If the dependency specification has an image ID, it will be compared against the hash of image returned, and must match. | |
256 | Otherwise, the labels in the dependency specification are compared against the labels in the retrieved ACI (i.e. in its ImageManifest), and must match. | |
257 | A label is considered to match if it meets one of two criteria: | |
258 | - It is present in the dependency specification and present in the dependency's ImageManifest with the same value. | |
259 | - It is absent from the dependency specification and present in the dependency's ImageManifest, with any value. | |
256 | First, the image discovery mechanism is used to locate a dependency based on the **imageName** and **labels** (see [App Container Image Discovery](discovery.md)). | |
257 | ||
258 | If the image discovery process successfully returns an image and the dependency specification has an image ID, it will be compared against the hash of image returned, and MUST match. | |
259 | ||
260 | 260 | This facilitates "wildcard" matching and a variety of common usage patterns, like "noarch" or "latest" dependencies. |
261 | 261 | For example, an ACI containing a set of bash scripts might omit both "os" and "arch", and hence could be used as a dependency by a variety of different ACIs. |
262 | 262 | Alternatively, an ACI might specify a dependency with no image ID and no "version" label, and the image discovery mechanism could always retrieve the latest version of an ACI. |
99 | 99 | Implementations of the spec are responsible for enforcing any signature validation rules set in place by the operator. |
100 | 100 | For example, in a testing environment, signature validation might be disabled, in which case the implementation would omit the signature retrieval. |
101 | 101 | |
102 | Implementations must ensure that the name in the Image Manifest in the retrieved ACI matches the initial name used for discovery. | |
102 | Implementations MUST ensure that the initial name and labels used for discovery matches the name and labels in the Image Manifest. | |
103 | ||
104 | A label is considered to match if it meets one of two criteria: | |
105 | - It is present in the discovery labels and present in the Image Manifest with the same value. | |
106 | - It is absent from the discovery labels and present in Image Manifest, with any value. | |
103 | 107 | |
104 | 108 | ### Authentication |
105 | 109 |
27 | 27 | |
28 | 28 | ```json |
29 | 29 | { |
30 | "acVersion": "0.6.1", | |
30 | "acVersion": "0.7.1", | |
31 | 31 | "acKind": "PodManifest", |
32 | 32 | "apps": [ |
33 | 33 | { |
53 | 53 | "mountPoints": [ |
54 | 54 | { |
55 | 55 | "name": "work", |
56 | "path": "/mnt/foo" | |
56 | "path": "/var/lib/work" | |
57 | 57 | } |
58 | 58 | ] |
59 | 59 | }, |
60 | 60 | "mounts": [ |
61 | {"volume": "work", "mountPoint": "work"} | |
61 | { | |
62 | "volume": "worklib", | |
63 | "path": "/var/lib/work" | |
64 | } | |
62 | 65 | ] |
63 | 66 | }, |
64 | 67 | { |
93 | 96 | ] |
94 | 97 | }, |
95 | 98 | "mounts": [ |
96 | {"volume": "work", "mountPoint": "backup"} | |
99 | { | |
100 | "volume": "worklib", | |
101 | "path": "/mnt/bar" | |
102 | } | |
97 | 103 | ], |
98 | 104 | "annotations": [ |
99 | 105 | { |
118 | 124 | ], |
119 | 125 | "volumes": [ |
120 | 126 | { |
121 | "name": "work", | |
127 | "name": "worklib", | |
122 | 128 | "kind": "host", |
123 | 129 | "source": "/opt/tenant1/work", |
124 | 130 | "readOnly": true |
157 | 163 | * **labels** (list of objects, optional) additional labels characterizing the image |
158 | 164 | * **app** (object, optional) substitute for the app object of the referred image's ImageManifest. See [Image Manifest Schema](aci.md#image-manifest-schema) for what the app object contains. |
159 | 165 | * **mounts** (list of objects, optional) list of mounts mapping an app mountPoint to a volume. Each mount has the following set of key-value pairs: |
160 | * **volume** (string, required) name of the volume that will fulfill this mount (restricted to the [AC Name](types.md#ac-name-type) formatting) | |
161 | * **mountPoint** (string, required) name of the app mount point to place the volume on (restricted to the [AC Name](types.md#ac-name-type) formatting) | |
166 | * **volume** (string, required) name of the volume that will fulfill this mount (restricted to the [AC Name](types.md#ac-name-type) formatting); this is a key into the list of `volumes`, below. | |
167 | * **path** (string, required) path inside the app filesystem to mount the volume; generally this will come from one of an app's mountPoint paths. For example, if an app has a mountPoint named "work" with path "/var/lib/work", an executor should map an appropriate volume to fulfill that mountPoint by using a `mount` object with that path. | |
162 | 168 | * **annotations** (list of objects, optional) arbitrary metadata appended to the app. The annotation objects must have a *name* key that has a value that is restricted to the [AC Name](types.md#ac-name-type) formatting and *value* key that is an arbitrary string). Annotation names must be unique within the list. These will be merged with annotations provided by the image manifest when queried via the metadata service; values in this list take precedence over those in the image manifest. |
163 | 169 | * **volumes** (list of objects, optional) list of volumes which will be mounted into each application's filesystem |
164 | * **name** (string, required) used to map the volume to an app's mountPoint at runtime. (restricted to the [AC Name](types.md#ac-name-type) formatting) | |
165 | * **kind** (string, required) either "empty" or "host". "empty" fulfills a mount point by ensuring the path exists (i.e., writes go to the app's chroot). "host" fulfills a mount point with a bind mount from a **source**. | |
170 | * **name** (string, required) descriptive label for the volume (restricted to the [AC Name](types.md#ac-name-type) formatting), used as an index by the `mounts` objects (above). | |
171 | * **readOnly** (boolean, optional, defaults to "false" if unsupplied) whether or not the volume will be mounted read only. | |
172 | * **kind** (string, required) either: | |
173 | * **empty** - creates an empty directory on the host and bind mounts it into the container. All containers in the pod share the mount, and the lifetime of the volume is equal to the lifetime of the pod (i.e. the directory on the host machine is removed when the pod's filesystem is garbage collected) | |
174 | * **host** - fulfills a mount point with a bind mount from a **source** directory on the host. | |
166 | 175 | * **source** (string, required if **kind** is "host") absolute path on host to be bind mounted under a mount point in each app's chroot. |
167 | * **readOnly** (boolean, optional if **kind** is "host", defaults to "false" if unsupplied) whether or not the volume will be mounted read only. | |
168 | 176 | * **isolators** (list of objects of type [Isolator](types.md#isolator-type), optional) list of isolation steps that will apply to this pod. |
169 | 177 | * **annotations** (list of objects, optional) arbitrary metadata the executor will make available to applications via the metadata service. Objects must contain two key-value pairs: **name** is restricted to the [AC Name](types.md#ac-name-type) formatting and **value** is an arbitrary string). Annotation names must be unique within the list. |
170 | 178 | * **ports** (list of objects, optional) list of ports that SHOULD be exposed on the host. |
15 | 15 | |
16 | 16 | source ./build |
17 | 17 | |
18 | TESTABLE_AND_FORMATTABLE="aci discovery pkg/acirenderer pkg/tarheader schema schema/types" | |
18 | TESTABLE_AND_FORMATTABLE="aci discovery pkg/acirenderer pkg/tarheader schema schema/lastditch schema/types" | |
19 | 19 | FORMATTABLE="$TESTABLE_AND_FORMATTABLE ace actool" |
20 | 20 | |
21 | 21 | # user has not provided PKG override |