Skip to content

Commit

Permalink
allow listing of all images of the same os (#205)
Browse files Browse the repository at this point in the history
  • Loading branch information
majst01 authored Jul 26, 2021
1 parent 60124e4 commit a26e5a6
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 0 deletions.
57 changes: 57 additions & 0 deletions cmd/metal-api/internal/datastore/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ func (rs *RethinkStore) GetImage(id string) (*metal.Image, error) {
return &i, nil
}

// FindImages returns all images for the given image id.
func (rs *RethinkStore) FindImages(id string) ([]metal.Image, error) {
allImages, err := rs.ListImages()
if err != nil {
return nil, err
}
imgs, err := getImagesFor(id, allImages)
if err != nil {
return nil, metal.NotFound("no images for id:%s found:%v", id, err)
}

return imgs, nil
}

// FindImage returns an image for the given image id.
func (rs *RethinkStore) FindImage(id string) (*metal.Image, error) {
allImages, err := rs.ListImages()
Expand Down Expand Up @@ -165,6 +179,49 @@ func (rs *RethinkStore) getMostRecentImageFor(id string, images metal.Images) (*
return nil, fmt.Errorf("no image for os:%s version:%s found", os, sv)
}

// getImagesFor
// the id is in the form of: <name>-<version>
// where name is for example ubuntu or firewall
// version must be a semantic version, see https://semver.org/
// we decided to specify the version in the form of major.minor.patch,
// where patch is in the form of YYYYMMDD
// If version is not fully specified, e.g. ubuntu-19.10 or ubuntu-19.10
// then all ubuntu images (ubuntu-19.10.*) are returned
// If patch is specified e.g. ubuntu-20.04.20200502 then this exact image is searched.
func getImagesFor(id string, images metal.Images) ([]metal.Image, error) {
os, sv, err := utils.GetOsAndSemverFromImage(id)
if err != nil {
return nil, err
}

matcher := "~"
// if patch is given return a exact match
if sv.Patch() > 0 {
matcher = "="
}
constraint, err := semver.NewConstraint(matcher + sv.String())
if err != nil {
return nil, fmt.Errorf("could not create constraint of image version:%s err:%w", sv, err)
}

result := []metal.Image{}
for i := range images {
image := images[i]
if os != image.OS {
continue
}
v, err := semver.NewVersion(image.Version)
if err != nil {
continue
}
if !constraint.Check(v) {
continue
}
result = append(result, image)
}
return result, nil
}

func sortImages(images []metal.Image) []metal.Image {
sort.SliceStable(images, func(i, j int) bool {
c := strings.Compare(images[i].OS, images[j].OS)
Expand Down
77 changes: 77 additions & 0 deletions cmd/metal-api/internal/datastore/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,83 @@ func Test_getMostRecentImageForFirewall(t *testing.T) {
}
}

func Test_getImagesFor(t *testing.T) {
valid := time.Now().Add(time.Hour)
ubuntu14_1 := metal.Image{Base: metal.Base{ID: "ubuntu-14.1"}, OS: "ubuntu", Version: "14.1", ExpirationDate: valid}
ubuntu14_04 := metal.Image{Base: metal.Base{ID: "ubuntu-14.04"}, OS: "ubuntu", Version: "14.04", ExpirationDate: valid}
ubuntu17_04 := metal.Image{Base: metal.Base{ID: "ubuntu-17.04"}, OS: "ubuntu", Version: "17.04", ExpirationDate: valid}
ubuntu17_10 := metal.Image{Base: metal.Base{ID: "ubuntu-17.10"}, OS: "ubuntu", Version: "17.10", ExpirationDate: valid}
ubuntu18_04 := metal.Image{Base: metal.Base{ID: "ubuntu-18.04"}, OS: "ubuntu", Version: "18.04", ExpirationDate: valid}
ubuntu19_04 := metal.Image{Base: metal.Base{ID: "ubuntu-19.04"}, OS: "ubuntu", Version: "19.4", ExpirationDate: valid}
ubuntu19_10 := metal.Image{Base: metal.Base{ID: "ubuntu-19.10"}, OS: "ubuntu", Version: "19.10", ExpirationDate: valid}
ubuntu20_04_20200401 := metal.Image{Base: metal.Base{ID: "ubuntu-20.04.20200401"}, OS: "ubuntu", Version: "20.04.20200401", ExpirationDate: valid}
ubuntu20_04_20200501 := metal.Image{Base: metal.Base{ID: "ubuntu-20.04.20200501"}, OS: "ubuntu", Version: "20.04.20200501", ExpirationDate: valid}
ubuntu20_04_20200502 := metal.Image{Base: metal.Base{ID: "ubuntu-20.04.20200502"}, OS: "ubuntu", Version: "20.04.20200502", ExpirationDate: valid}
ubuntu20_04_20200603 := metal.Image{Base: metal.Base{ID: "ubuntu-20.04.20200603"}, OS: "ubuntu", Version: "20.04.20200603", ExpirationDate: valid}

alpine3_9 := metal.Image{Base: metal.Base{ID: "alpine-3.9"}, OS: "alpine", Version: "3.9", ExpirationDate: valid}
alpine3_9_20191012 := metal.Image{Base: metal.Base{ID: "alpine-3.9.20191012"}, OS: "alpine", Version: "3.9.20191012", ExpirationDate: valid}
alpine3_10 := metal.Image{Base: metal.Base{ID: "alpine-3.10"}, OS: "alpine", Version: "3.10", ExpirationDate: valid}
alpine3_10_20191012 := metal.Image{Base: metal.Base{ID: "alpine-3.10.20191012"}, OS: "alpine", Version: "3.10.20191012", ExpirationDate: valid}
tests := []struct {
name string
id string
images []metal.Image
want []metal.Image
wantErr bool
}{
{
name: "simple",
id: "ubuntu-20.04",
images: []metal.Image{ubuntu20_04_20200502, ubuntu19_10, ubuntu17_04, ubuntu20_04_20200401, ubuntu19_04, ubuntu14_1, ubuntu20_04_20200501, ubuntu18_04, ubuntu14_04, ubuntu17_10, ubuntu20_04_20200603},
want: []metal.Image{ubuntu20_04_20200502, ubuntu20_04_20200401, ubuntu20_04_20200501, ubuntu20_04_20200603},
wantErr: false,
},
{
name: "patch given with no match",
id: "ubuntu-20.04.2020",
images: []metal.Image{ubuntu20_04_20200502, alpine3_9, ubuntu19_10, ubuntu17_04, ubuntu20_04_20200401, ubuntu19_04, ubuntu14_1, ubuntu20_04_20200501, ubuntu18_04, ubuntu14_04, ubuntu17_10, ubuntu20_04_20200603, alpine3_9_20191012},
want: []metal.Image{},
wantErr: false,
},
{
name: "patch given with match",
id: "ubuntu-20.04.20200502",
images: []metal.Image{ubuntu20_04_20200502, alpine3_9, ubuntu19_10, ubuntu17_04, ubuntu20_04_20200401, ubuntu19_04, ubuntu14_1, ubuntu20_04_20200501, ubuntu18_04, ubuntu14_04, ubuntu17_10, ubuntu20_04_20200603, alpine3_9_20191012},
want: []metal.Image{ubuntu20_04_20200502},
wantErr: false,
},
{
name: "alpine",
id: "alpine-3.10",
images: []metal.Image{ubuntu20_04_20200502, alpine3_9, ubuntu19_10, ubuntu17_04, alpine3_10_20191012, ubuntu20_04_20200401, ubuntu19_04, ubuntu14_1, ubuntu20_04_20200501, ubuntu18_04, alpine3_10, ubuntu14_04, ubuntu17_10, ubuntu20_04_20200603, alpine3_9_20191012},
want: []metal.Image{alpine3_10_20191012, alpine3_10},
wantErr: false,
},
{
name: "alpine II",
id: "alpine-3.9",
images: []metal.Image{ubuntu20_04_20200502, alpine3_9, ubuntu19_10, ubuntu17_04, alpine3_10_20191012, ubuntu20_04_20200401, ubuntu19_04, ubuntu14_1, ubuntu20_04_20200501, ubuntu18_04, alpine3_10, ubuntu14_04, ubuntu17_10, ubuntu20_04_20200603, alpine3_9_20191012},
want: []metal.Image{alpine3_9, alpine3_9_20191012},
wantErr: false,
},
}

for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
got, err := getImagesFor(tt.id, tt.images)
if (err != nil) != tt.wantErr {
t.Errorf("getImagesFor() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("getImagesFor() %s\n", cmp.Diff(got, tt.want))
}
})
}
}

func Test_sortImages(t *testing.T) {
firewall2 := metal.Image{Base: metal.Base{ID: "firewall-2.0.20200331"}, OS: "firewall", Version: "2.0.20200331"}
firewallubuntu2 := metal.Image{Base: metal.Base{ID: "firewall-ubuntu-2.0.20200331"}, OS: "firewall-ubuntu", Version: "2.0.20200331"}
Expand Down
29 changes: 29 additions & 0 deletions cmd/metal-api/internal/service/image-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ func (ir imageResource) webService() *restful.WebService {
Returns(http.StatusOK, "OK", v1.ImageResponse{}).
DefaultReturns("Error", httperrors.HTTPErrorResponse{}))

ws.Route(ws.GET("/{id}/query").
To(ir.queryImages).
Operation("queryImages by id").
Doc("query all images which match at least id").
Param(ws.PathParameter("id", "identifier of the image").DataType("string")).
Metadata(restfulspec.KeyOpenAPITags, tags).
Writes([]v1.ImageResponse{}).
Returns(http.StatusOK, "OK", []v1.ImageResponse{}).
DefaultReturns("Error", httperrors.HTTPErrorResponse{}))

ws.Route(ws.GET("/{id}/latest").
To(ir.findLatestImage).
Operation("findLatestImage").
Expand Down Expand Up @@ -123,6 +133,25 @@ func (ir imageResource) findImage(request *restful.Request, response *restful.Re
}
}

func (ir imageResource) queryImages(request *restful.Request, response *restful.Response) {
id := request.PathParameter("id")

img, err := ir.ds.FindImages(id)
if checkError(request, response, utils.CurrentFuncName(), err) {
return
}
result := []*v1.ImageResponse{}

for i := range img {
result = append(result, v1.NewImageResponse(&img[i]))
}
err = response.WriteHeaderAndEntity(http.StatusOK, result)
if err != nil {
zapup.MustRootLogger().Error("Failed to send response", zap.Error(err))
return
}
}

func (ir imageResource) findLatestImage(request *restful.Request, response *restful.Response) {
id := request.PathParameter("id")

Expand Down
41 changes: 41 additions & 0 deletions spec/metal-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -5111,6 +5111,47 @@
]
}
},
"/v1/image/{id}/query": {
"get": {
"consumes": [
"application/json"
],
"operationId": "queryImages by id",
"parameters": [
{
"description": "identifier of the image",
"in": "path",
"name": "id",
"required": true,
"type": "string"
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "OK",
"schema": {
"items": {
"$ref": "#/definitions/v1.ImageResponse"
},
"type": "array"
}
},
"default": {
"description": "Error",
"schema": {
"$ref": "#/definitions/httperrors.HTTPErrorResponse"
}
}
},
"summary": "query all images which match at least id",
"tags": [
"image"
]
}
},
"/v1/ip": {
"get": {
"consumes": [
Expand Down

0 comments on commit a26e5a6

Please sign in to comment.