Skip to content

Commit fdaa567

Browse files
committed
starlark/module: experimental/docker module to work with docker images and semver
1 parent 0bb0432 commit fdaa567

File tree

7 files changed

+446
-0
lines changed

7 files changed

+446
-0
lines changed

_examples/docker.star

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
load("experimental/docker", "docker")
2+
3+
p = provider("docker", "2.7.0", "foo")
4+
5+
# using docker.image semver can be used to choose the docker image, `
6+
golang = docker.image("golang", "1.13.x")
7+
8+
foo = p.resource.container("foo")
9+
foo.name = "foo"
10+
11+
# version queries the docker repository and returns the correct tag.
12+
foo.image = golang.version(full=True)
13+
14+
print(hcl(p))

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ module github.com/mcuadros/ascode
33
go 1.14
44

55
require (
6+
github.com/Masterminds/semver/v3 v3.0.3
67
github.com/b5/outline v0.0.0-20190307020728-8cdd78996e40
8+
github.com/containers/image/v5 v5.2.1
9+
github.com/docker/distribution v2.7.1+incompatible // indirect
10+
github.com/docker/go-metrics v0.0.1 // indirect
711
github.com/go-git/go-git/v5 v5.0.0
812
github.com/hashicorp/go-hclog v0.9.2
913
github.com/hashicorp/go-plugin v1.0.1

go.sum

Lines changed: 146 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package docker
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"sort"
8+
"sync"
9+
10+
"github.com/Masterminds/semver/v3"
11+
"github.com/containers/image/v5/docker"
12+
"github.com/containers/image/v5/docker/reference"
13+
"github.com/containers/image/v5/types"
14+
"go.starlark.net/starlark"
15+
"go.starlark.net/starlarkstruct"
16+
)
17+
18+
const (
19+
// ModuleName defines the expected name for this Module when used
20+
// in starlark's load() function, eg: load('experimental/docker', 'docker')
21+
ModuleName = "experimental/docker"
22+
23+
ImageFuncName = "image"
24+
25+
latestTag = "lastest"
26+
)
27+
28+
var (
29+
once sync.Once
30+
dockerModule starlark.StringDict
31+
)
32+
33+
// LoadModule loads the os module.
34+
// It is concurrency-safe and idempotent.
35+
//
36+
// outline: docker
37+
// path: experimental/docker
38+
func LoadModule() (starlark.StringDict, error) {
39+
once.Do(func() {
40+
dockerModule = starlark.StringDict{
41+
"docker": &starlarkstruct.Module{
42+
Name: "docker",
43+
Members: starlark.StringDict{
44+
ImageFuncName: starlark.NewBuiltin(ImageFuncName, Image),
45+
},
46+
},
47+
}
48+
})
49+
50+
return dockerModule, nil
51+
}
52+
53+
type sString = starlark.String
54+
type image struct {
55+
tags []string
56+
ref types.ImageReference
57+
constraint string
58+
sString
59+
}
60+
61+
func Image(
62+
thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple,
63+
) (starlark.Value, error) {
64+
65+
var image, constraint string
66+
err := starlark.UnpackArgs(ImageFuncName, args, kwargs, "image", &image, "constraint", &constraint)
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
return newImage(image, constraint)
72+
}
73+
74+
func newImage(name, constraint string) (*image, error) {
75+
ref, err := reference.ParseNormalizedNamed(name)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
if !reference.IsNameOnly(ref) {
81+
return nil, errors.New("no tag or digest allowed in reference")
82+
}
83+
84+
dref, err := docker.NewReference(reference.TagNameOnly(ref))
85+
if err != nil {
86+
return nil, err
87+
}
88+
89+
return &image{
90+
ref: dref,
91+
constraint: constraint,
92+
sString: starlark.String(ref.Name()),
93+
}, nil
94+
}
95+
96+
func (i *image) Attr(name string) (starlark.Value, error) {
97+
switch name {
98+
case "name":
99+
return starlark.String(i.ref.DockerReference().Name()), nil
100+
case "tags":
101+
return i.getTags()
102+
case "version":
103+
return starlark.NewBuiltin("version", i.builtinVersionFunc), nil
104+
}
105+
106+
return nil, nil
107+
}
108+
109+
func (i *image) builtinVersionFunc(
110+
_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple,
111+
) (starlark.Value, error) {
112+
113+
var full bool
114+
starlark.UnpackArgs(ImageFuncName, args, kwargs, "full", &full)
115+
116+
v, err := i.getVersion()
117+
if err != nil {
118+
return starlark.None, err
119+
}
120+
121+
if full {
122+
v = fmt.Sprintf("%s:%s", i.ref.DockerReference().Name(), v)
123+
}
124+
125+
return starlark.String(v), nil
126+
}
127+
128+
func (i *image) getTags() (*starlark.List, error) {
129+
if len(i.tags) != 0 {
130+
return listToStarlark(i.tags), nil
131+
}
132+
133+
var err error
134+
i.tags, err = docker.GetRepositoryTags(context.TODO(), nil, i.ref)
135+
if err != nil {
136+
return nil, fmt.Errorf("error listing repository tags: %v", err)
137+
}
138+
139+
i.tags = sortTags(i.tags)
140+
return listToStarlark(i.tags), nil
141+
}
142+
143+
func (i *image) getVersion() (string, error) {
144+
if i.constraint == latestTag {
145+
return latestTag, nil
146+
}
147+
148+
_, err := i.getTags()
149+
if err != nil {
150+
return "", err
151+
}
152+
153+
if len(i.tags) == 0 {
154+
return "", fmt.Errorf("no tags form this image")
155+
}
156+
157+
c, err := semver.NewConstraint(i.constraint)
158+
if err != nil {
159+
return i.doGetVersionExactTag(i.constraint)
160+
}
161+
162+
return i.doGetVersionWithConstraint(c)
163+
}
164+
165+
func (i *image) doGetVersionWithConstraint(c *semver.Constraints) (string, error) {
166+
// it assumes tags are always sorted from higher to lower
167+
for _, tag := range i.tags {
168+
v, err := semver.NewVersion(tag)
169+
if err == nil {
170+
if c.Check(v) {
171+
return tag, nil
172+
}
173+
}
174+
}
175+
176+
return "", nil
177+
}
178+
179+
func (i *image) doGetVersionExactTag(expected string) (string, error) {
180+
for _, tag := range i.tags {
181+
if tag == expected {
182+
return tag, nil
183+
}
184+
}
185+
186+
return "", fmt.Errorf("tag %q not found in repository", expected)
187+
}
188+
189+
func sortTags(tags []string) []string {
190+
versions, others := listToVersion(tags)
191+
sort.Sort(sort.Reverse(semver.Collection(versions)))
192+
return versionToList(versions, others)
193+
}
194+
195+
func listToStarlark(input []string) *starlark.List {
196+
output := make([]starlark.Value, len(input))
197+
for i, v := range input {
198+
output[i] = starlark.String(v)
199+
}
200+
201+
return starlark.NewList(output)
202+
}
203+
204+
func listToVersion(input []string) ([]*semver.Version, []string) {
205+
versions := make([]*semver.Version, 0)
206+
other := make([]string, 0)
207+
208+
for _, text := range input {
209+
v, err := semver.NewVersion(text)
210+
if err == nil && v.Prerelease() == "" {
211+
versions = append(versions, v)
212+
continue
213+
}
214+
215+
other = append(other, text)
216+
}
217+
218+
return versions, other
219+
}
220+
221+
func versionToList(versions []*semver.Version, other []string) []string {
222+
output := make([]string, 0)
223+
for _, v := range versions {
224+
output = append(output, v.Original())
225+
}
226+
227+
return append(output, other...)
228+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package docker
2+
3+
import (
4+
"testing"
5+
6+
"github.com/qri-io/starlib/testdata"
7+
"go.starlark.net/resolve"
8+
"go.starlark.net/starlark"
9+
"go.starlark.net/starlarktest"
10+
)
11+
12+
func TestImage(t *testing.T) {
13+
resolve.AllowFloat = true
14+
thread := &starlark.Thread{Load: testdata.NewLoader(LoadModule, ModuleName)}
15+
starlarktest.SetReporter(thread, t)
16+
17+
// Execute test file
18+
_, err := starlark.ExecFile(thread, "testdata/test.star", nil, nil)
19+
if err != nil {
20+
if ee, ok := err.(*starlark.EvalError); ok {
21+
t.Error(ee.Backtrace())
22+
} else {
23+
t.Error(err)
24+
}
25+
}
26+
27+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
load('experimental/docker', 'docker')
2+
load('assert.star', 'assert')
3+
4+
image = docker.image("fedora", "latest")
5+
assert.eq(image.name, "docker.io/library/fedora")
6+
assert.eq(image.version(), "latest")
7+
8+
full = docker.image("fedora", "24")
9+
assert.eq(full.name, "docker.io/library/fedora")
10+
assert.eq(full.version(True), "docker.io/library/fedora:24")
11+
12+
semver = docker.image("fedora", ">=22 <30")
13+
assert.eq(semver.name, "docker.io/library/fedora")
14+
assert.eq(semver.version(), "29")
15+
16+
golang = docker.image("golang", "1.13.x")
17+
assert.eq(golang.name, "docker.io/library/golang")
18+
assert.eq(golang.version(), "1.13.8")
19+
20+
tagNotFound = docker.image("fedora", "not-found")
21+
assert.eq(tagNotFound.name, "docker.io/library/fedora")
22+
23+
def tagNotExistant(): tagNotFound.version()
24+
assert.fails(tagNotExistant,'tag "not-found" not found in repository')

starlark/runtime/runtime.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package runtime
33
import (
44
"fmt"
55

6+
"github.com/mcuadros/ascode/starlark/module/experimental/docker"
67
"github.com/mcuadros/ascode/starlark/module/filepath"
78
"github.com/mcuadros/ascode/starlark/module/os"
89
"github.com/mcuadros/ascode/starlark/types"
@@ -45,6 +46,8 @@ func NewRuntime(pm *terraform.PluginManager) *Runtime {
4546
"encoding/yaml": yaml.LoadModule,
4647
"re": re.LoadModule,
4748
"http": http.LoadModule,
49+
50+
"experimental/docker": docker.LoadModule,
4851
},
4952
predeclared: starlark.StringDict{
5053
"provider": types.BuiltinProvider(pm),

0 commit comments

Comments
 (0)