Skip to content

Commit d1948d7

Browse files
committedJan 16, 2020
Add support for choosing the package's release
Up until now, the appregistry-server always pulled the latest version of the requested packages, which makes sense for production, but not for testing and release phases of the packages. During testing, multiple version of the same package can be tested at the same time, and when releasing, it's not mandatory that the different releases will be released on the same date. When combining the appregistry-server logic, with the properties of the test and release phases, there is a need to create a complex automation for merging multiple versions (so they can be tested at the time) into a single bundle, and omitting non-ready releases from bundles, after testing and before the release. This change adds the possibility to specify the requested release after the package's name (e.g "Kubevirt:10.0.0"). If the version is not specified, the server will download the latest version from the appregistry. Signed-off-by: gbenhaim <[email protected]>
1 parent 5ec9f93 commit d1948d7

File tree

8 files changed

+291
-51
lines changed

8 files changed

+291
-51
lines changed
 

‎cmd/appregistry-server/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func main() {
3737
rootCmd.Flags().StringP("download-folder", "f", "downloaded", "directory where downloaded nested operator bundle(s) will be stored to be processed further")
3838
rootCmd.Flags().StringP("database", "d", "bundles.db", "name of db to output")
3939
rootCmd.Flags().StringSliceP("registry", "r", []string{}, "pipe delimited operator source - {base url with cnr prefix}|{quay registry namespace}|{secret namespace/secret name}")
40-
rootCmd.Flags().StringP("packages", "o", "", "comma separated list of package(s) to be downloaded from the specified operator source(s)")
40+
rootCmd.Flags().StringP("packages", "o", "", "comma separated list of package(s) to be downloaded from the specified operator source(s). The requested release can be appended to the package name, delimited with a colone (e.g some-pkg:1.1.0)")
4141
rootCmd.Flags().StringP("port", "p", "50051", "port number to serve on")
4242
rootCmd.Flags().StringP("termination-log", "t", "/dev/termination-log", "path to a container termination log file")
4343
rootCmd.Flags().Bool("strict", false, "fail on registry load errors")

‎go.sum

+1
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
487487
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
488488
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
489489
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
490+
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
490491
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
491492
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
492493
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=

‎pkg/apprclient/client.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ func (c *client) ListPackages(namespace string) ([]*RegistryMetadata, error) {
6464
Name: repository,
6565

6666
// 'Default' points to the latest release pushed.
67-
Release: pkg.Default,
67+
Release: pkg.Default,
68+
Releases: pkg.Releases,
6869

6970
// Getting 'Digest' would require an additional call to the app
7071
// registry, so it is being defaulted.

‎pkg/apprclient/metadata.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@ type RegistryMetadata struct {
3131
// registry.
3232
Name string
3333

34-
// Release represents the version number of the given operator manifest.
34+
// Release represents the latest version number of the given operator manifest.
3535
Release string
3636

37+
// Releases represents all the available releases of the given operator manifest
38+
Releases []string
39+
3740
// Digest is the sha256 hash value that uniquely corresponds to the blob
3841
// associated with this particular release of the operator manifest.
3942
Digest string
@@ -47,3 +50,15 @@ func (rm *RegistryMetadata) ID() string {
4750
func (rm *RegistryMetadata) String() string {
4851
return fmt.Sprintf("%s/%s:%s", rm.Namespace, rm.Name, rm.Release)
4952
}
53+
54+
// ReleaseMap returns a map between all the available releases of a package to
55+
// a bool, usefull for checking is some release is available for a package.
56+
func (rm *RegistryMetadata) ReleaseMap() map[string]bool {
57+
releases := map[string]bool{}
58+
59+
for _, release := range rm.Releases {
60+
releases[release] = false
61+
}
62+
63+
return releases
64+
}

‎pkg/appregistry/downloader.go

+44-20
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ type downloadItem struct {
2020

2121
// Source refers to the remote appregistry URL and remote registry namespace.
2222
Source *Source
23+
24+
// Release refers to the release number the user requested
25+
Release string
2326
}
2427

2528
func (d *downloadItem) String() string {
26-
return fmt.Sprintf("%s", d.RepositoryMetadata)
29+
return fmt.Sprintf("%s:%s", d.RepositoryMetadata.Name, d.Release)
2730
}
2831

2932
type registryOptionsGetter interface {
@@ -73,11 +76,18 @@ func (d *downloader) Download(input *Input) (manifests []*apprclient.OperatorMet
7376
d.logger.Errorf("the following error(s) occurred while preparing the download list: %v", err)
7477

7578
if len(items) == 0 {
76-
d.logger.Infof("download list is empty, bailing out: %s", input.Packages)
79+
d.logger.Infof("download list is empty, bailing out: %v", input.Packages)
7780
return
7881
}
7982
}
8083

84+
for _, item := range items {
85+
d.logger.Infof(
86+
"the following releases are available for package %s -> %s",
87+
item.RepositoryMetadata.Name,
88+
item.RepositoryMetadata.Releases,
89+
)
90+
}
8191
d.logger.Infof("resolved the following packages: %s", items)
8292

8393
manifests, err = d.DownloadRepositories(items)
@@ -95,7 +105,7 @@ func (d *downloader) Download(input *Input) (manifests []*apprclient.OperatorMet
95105
// log it and move on.
96106
func (d *downloader) Prepare(input *Input) (items []*downloadItem, err error) {
97107
packageMap := input.PackagesToMap()
98-
itemMap := map[string]*downloadItem{}
108+
itemMap := map[Package]*downloadItem{}
99109
allErrors := []error{}
100110

101111
for _, source := range input.Sources {
@@ -112,22 +122,36 @@ func (d *downloader) Prepare(input *Input) (items []*downloadItem, err error) {
112122
continue
113123
}
114124

125+
repositoryMap := map[string]*apprclient.RegistryMetadata{}
115126
for _, metadata := range repositoryList {
116-
// Repository name has a one to one mapping to operator/package name.
117-
// We use this as the key.
118-
key := metadata.Name
119-
120-
if _, ok := packageMap[key]; ok {
121-
// The package specified has been resolved to this repository
122-
// name in remote registry.
123-
itemMap[key] = &downloadItem{
124-
RepositoryMetadata: metadata,
125-
Source: source,
127+
repositoryMap[metadata.Name] = metadata
128+
}
129+
130+
for _, pkg := range input.Packages {
131+
metadata, ok := repositoryMap[pkg.Name]
132+
if !ok {
133+
// The package is not in the current source
134+
continue
135+
}
136+
// If a specific release was requrested, download it
137+
release := pkg.Release
138+
if release != "" {
139+
releaseMap := metadata.ReleaseMap()
140+
if _, ok := releaseMap[pkg.Release]; !ok {
141+
// We have the package, but not the requested release
142+
continue
126143
}
144+
} else {
145+
// default to the latest
146+
release = metadata.Release
147+
}
127148

128-
// Remove the package specified since it has been resolved.
129-
delete(packageMap, key)
149+
itemMap[*pkg] = &downloadItem{
150+
RepositoryMetadata: metadata,
151+
Release: release,
152+
Source: source,
130153
}
154+
delete(packageMap, *pkg)
131155
}
132156
}
133157

@@ -154,28 +178,28 @@ func (d *downloader) DownloadRepositories(items []*downloadItem) (manifests []*a
154178
for _, item := range items {
155179
endpoint := item.Source.Endpoint
156180

157-
d.logger.Infof("downloading repository: %s from %s", item.RepositoryMetadata, endpoint)
181+
d.logger.Infof("downloading repository: %s from %s", item, endpoint)
158182

159183
options, err := d.regOptionGetter.GetRegistryOptions(item.Source)
160184
if err != nil {
161185
allErrors = append(allErrors, err)
162-
d.logger.Infof("skipping repository: %s", item.RepositoryMetadata)
186+
d.logger.Infof("skipping repository: %s", item)
163187

164188
continue
165189
}
166190

167191
client, err := apprclient.New(*options)
168192
if err != nil {
169193
allErrors = append(allErrors, err)
170-
d.logger.Infof("skipping repository: %s", item.RepositoryMetadata)
194+
d.logger.Infof("skipping repository: %s", item)
171195

172196
continue
173197
}
174198

175-
manifest, err := client.RetrieveOne(item.RepositoryMetadata.ID(), item.RepositoryMetadata.Release)
199+
manifest, err := client.RetrieveOne(item.RepositoryMetadata.ID(), item.Release)
176200
if err != nil {
177201
allErrors = append(allErrors, err)
178-
d.logger.Infof("skipping repository: %s", item.RepositoryMetadata)
202+
d.logger.Infof("skipping repository: %s", item)
179203

180204
continue
181205
}

‎pkg/appregistry/downloader_test.go

+100-11
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ var testPrepare = []struct {
2525
RegistryNamespace: "",
2626
},
2727
},
28-
Packages: []string{
29-
"Kubevirt",
30-
"etcd",
28+
Packages: []*Package{
29+
&Package{"Kubevirt", ""},
30+
&Package{"etcd", ""},
3131
},
3232
},
3333
sourceQuerier: &fakeSourceQuerier{
@@ -43,8 +43,9 @@ var testPrepare = []struct {
4343
},
4444
expectedDownloadItems: []*downloadItem{
4545
&downloadItem{
46-
&apprclient.RegistryMetadata{Name: "Kubevirt"},
47-
&Source{Endpoint: "quay.io", RegistryNamespace: ""},
46+
RepositoryMetadata: &apprclient.RegistryMetadata{Name: "Kubevirt"},
47+
Source: &Source{Endpoint: "quay.io", RegistryNamespace: ""},
48+
Release: "",
4849
},
4950
},
5051
expectedError: nil,
@@ -61,9 +62,9 @@ var testPrepare = []struct {
6162
RegistryNamespace: "",
6263
},
6364
},
64-
Packages: []string{
65-
"Kubevirt",
66-
"etcd",
65+
Packages: []*Package{
66+
&Package{"Kubevirt", ""},
67+
&Package{"etcd", ""},
6768
},
6869
},
6970
sourceQuerier: &fakeSourceQuerier{
@@ -84,8 +85,9 @@ var testPrepare = []struct {
8485
},
8586
expectedDownloadItems: []*downloadItem{
8687
&downloadItem{
87-
&apprclient.RegistryMetadata{Name: "Kubevirt"},
88-
&Source{Endpoint: "quay.io", RegistryNamespace: ""},
88+
RepositoryMetadata: &apprclient.RegistryMetadata{Name: "Kubevirt"},
89+
Source: &Source{Endpoint: "quay.io", RegistryNamespace: ""},
90+
Release: "",
8991
},
9092
},
9193
expectedError: utilerrors.NewAggregate(
@@ -94,6 +96,91 @@ var testPrepare = []struct {
9496
},
9597
),
9698
},
99+
{
100+
input: &Input{
101+
Sources: []*Source{
102+
{
103+
Endpoint: "quay.io",
104+
RegistryNamespace: "",
105+
},
106+
},
107+
Packages: []*Package{
108+
&Package{"Kubevirt", "10.0.0"},
109+
&Package{"etcd", ""},
110+
},
111+
},
112+
sourceQuerier: &fakeSourceQuerier{
113+
map[Source][]*apprclient.RegistryMetadata{
114+
Source{
115+
Endpoint: "quay.io",
116+
RegistryNamespace: "",
117+
}: {
118+
&apprclient.RegistryMetadata{
119+
Name: "Kubevirt",
120+
Release: "11.0.0",
121+
Releases: []string{"10.0.0", "11.0.0"},
122+
},
123+
&apprclient.RegistryMetadata{
124+
Name: "etcd",
125+
Release: "2.0.0",
126+
Releases: []string{"1.0.0", "2.0.0"},
127+
},
128+
},
129+
},
130+
map[Source]error{},
131+
},
132+
expectedDownloadItems: []*downloadItem{
133+
&downloadItem{
134+
RepositoryMetadata: &apprclient.RegistryMetadata{
135+
Name: "Kubevirt",
136+
Release: "11.0.0",
137+
Releases: []string{"10.0.0", "11.0.0"},
138+
},
139+
Source: &Source{Endpoint: "quay.io", RegistryNamespace: ""},
140+
Release: "10.0.0",
141+
},
142+
&downloadItem{
143+
RepositoryMetadata: &apprclient.RegistryMetadata{
144+
Name: "etcd",
145+
Release: "2.0.0",
146+
Releases: []string{"1.0.0", "2.0.0"},
147+
},
148+
Source: &Source{Endpoint: "quay.io", RegistryNamespace: ""},
149+
Release: "2.0.0",
150+
},
151+
},
152+
expectedError: nil,
153+
},
154+
{
155+
input: &Input{
156+
Sources: []*Source{
157+
{
158+
Endpoint: "quay.io",
159+
RegistryNamespace: "",
160+
},
161+
},
162+
Packages: []*Package{
163+
&Package{"Kubevirt", "10.0.0"},
164+
},
165+
},
166+
sourceQuerier: &fakeSourceQuerier{
167+
map[Source][]*apprclient.RegistryMetadata{
168+
Source{
169+
Endpoint: "quay.io",
170+
RegistryNamespace: "",
171+
}: {
172+
&apprclient.RegistryMetadata{
173+
Name: "Kubevirt",
174+
Release: "11.0.0",
175+
Releases: []string{"11.0.0"},
176+
},
177+
},
178+
},
179+
map[Source]error{},
180+
},
181+
expectedDownloadItems: []*downloadItem{},
182+
expectedError: nil,
183+
},
97184
}
98185

99186
type fakeSourceQuerier struct {
@@ -122,7 +209,9 @@ func TestPrepare(t *testing.T) {
122209
}
123210

124211
downloadItems, err := d.Prepare(testItem.input)
125-
assert.Equal(t, testItem.expectedDownloadItems, downloadItems)
212+
// Since downloadItems are stored in a map inside of the Prepare
213+
// function the order isn't guaranteed
214+
assert.ElementsMatch(t, testItem.expectedDownloadItems, downloadItems)
126215
if testItem.expectedError != nil {
127216
assert.Equal(t, testItem.expectedError, err)
128217
} else {

‎pkg/appregistry/input.go

+60-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package appregistry
22

33
import (
4+
"fmt"
45
"strings"
6+
7+
utilerrors "k8s.io/apimachinery/pkg/util/errors"
58
)
69

710
// OperatorSourceSpecifier interface provides capability to have different ways
@@ -17,18 +20,32 @@ type Input struct {
1720
Sources []*Source
1821

1922
// Packages is the set of package name(s) specified.
20-
Packages []string
23+
Packages []*Package
24+
}
25+
26+
type Package struct {
27+
// The name of the package
28+
Name string
29+
// The release number of the package
30+
Release string
31+
}
32+
33+
func (p *Package) String() string {
34+
if p.Release == "" {
35+
return fmt.Sprintf("%s", p.Name)
36+
}
37+
return fmt.Sprintf("%s:%s", p.Name, p.Release)
2138
}
2239

2340
func (i *Input) IsGoodToProceed() bool {
2441
return len(i.Sources) > 0 && len(i.Packages) > 0
2542
}
2643

27-
func (i *Input) PackagesToMap() map[string]bool {
28-
packages := map[string]bool{}
44+
func (i *Input) PackagesToMap() map[Package]bool {
45+
packages := map[Package]bool{}
2946

3047
for _, pkg := range i.Packages {
31-
packages[pkg] = false
48+
packages[*pkg] = false
3249
}
3350

3451
return packages
@@ -56,7 +73,7 @@ func (p *inputParser) Parse(csvSources []string, csvPackages string) (*Input, er
5673
return nil, err
5774
}
5875

59-
packages := sanitizePackageList(strings.Split(csvPackages, ","))
76+
packages, err := sanitizePackageList(strings.Split(csvPackages, ","))
6077

6178
return &Input{
6279
Sources: sources,
@@ -66,18 +83,49 @@ func (p *inputParser) Parse(csvSources []string, csvPackages string) (*Input, er
6683

6784
// sanitizePackageList sanitizes the set of package(s) specified. It removes
6885
// duplicates and ignores empty string.
69-
func sanitizePackageList(in []string) []string {
70-
out := make([]string, 0)
71-
86+
func sanitizePackageList(in []string) ([]*Package, error) {
87+
out := make([]*Package, 0)
88+
allErrors := []error{}
7289
inMap := map[string]bool{}
90+
7391
for _, item := range in {
74-
if _, ok := inMap[item]; ok || item == "" {
92+
name, release, err := getNameAndRelease(item)
93+
if err != nil {
94+
allErrors = append(allErrors, err)
95+
continue
96+
}
97+
98+
if _, ok := inMap[name]; ok || name == "" {
7599
continue
76100
}
77101

78-
inMap[item] = true
79-
out = append(out, item)
102+
inMap[name] = true
103+
out = append(out, &Package{Name: name, Release: release})
80104
}
81105

82-
return out
106+
err := utilerrors.NewAggregate(allErrors)
107+
return out, err
108+
}
109+
110+
func getNameAndRelease(in string) (string, string, error) {
111+
inWithoutSpaces := strings.Map(
112+
func(r rune) rune {
113+
if r == ' ' {
114+
return -1
115+
}
116+
return r
117+
},
118+
in,
119+
)
120+
parts := strings.Split(inWithoutSpaces, ":")
121+
122+
switch len(parts) {
123+
case 1:
124+
// release wasn't specified
125+
return parts[0], "", nil
126+
case 2:
127+
return parts[0], parts[1], nil
128+
default:
129+
return "", "", fmt.Errorf("Failed to parse package %s", in)
130+
}
83131
}

‎pkg/appregistry/input_test.go

+67-5
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,73 @@ import (
66
"github.com/stretchr/testify/assert"
77
)
88

9-
func TestDuplicatePackage(t *testing.T) {
9+
type testInput struct {
10+
csvSources string
11+
expectedPackages []*Package
12+
}
13+
14+
var testGoodInput = []testInput{
15+
{
16+
"Jaeger,Jaeger",
17+
[]*Package{
18+
&Package{Name: "Jaeger", Release: ""},
19+
},
20+
},
21+
{
22+
"Jaeger,Kubevirt:10.0.0",
23+
[]*Package{
24+
&Package{Name: "Jaeger", Release: ""},
25+
&Package{Name: "Kubevirt", Release: "10.0.0"},
26+
},
27+
},
28+
{
29+
"Jaeger,Kubevirt:10.0.0,Kubevirt",
30+
[]*Package{
31+
&Package{Name: "Jaeger", Release: ""},
32+
&Package{Name: "Kubevirt", Release: "10.0.0"},
33+
},
34+
},
35+
{
36+
"Jaeger :2.0.0, Kubevirt: 10.0.0, Kubevirt",
37+
[]*Package{
38+
&Package{Name: "Jaeger", Release: "2.0.0"},
39+
&Package{Name: "Kubevirt", Release: "10.0.0"},
40+
},
41+
},
42+
{
43+
"",
44+
[]*Package{},
45+
},
46+
}
47+
48+
func TestGoodInput(t *testing.T) {
49+
parser := inputParser{sourceSpecifier: &registrySpecifier{}}
50+
51+
for _, goodInput := range testGoodInput {
52+
actual, err := parser.Parse(
53+
[]string{"https://quay.io/cnr|community-operator|"},
54+
goodInput.csvSources,
55+
)
56+
assert.NoError(t, err)
57+
assert.Equal(t, goodInput.expectedPackages, actual.Packages)
58+
}
59+
}
60+
61+
var testFaultyInput = []testInput{
62+
{
63+
"Jaeger,Kubevirt:10.0.0:11.0.0",
64+
nil,
65+
},
66+
}
67+
68+
func TestFaultyInput(t *testing.T) {
1069
parser := inputParser{sourceSpecifier: &registrySpecifier{}}
11-
expectedPackages := []string{"Jaeger"}
1270

13-
actual, err := parser.Parse([]string{"https://quay.io/cnr|community-operator|"}, "Jaeger,Jaeger")
14-
assert.NoError(t, err)
15-
assert.Equal(t, expectedPackages, actual.Packages)
71+
for _, mfInput := range testFaultyInput {
72+
_, err := parser.Parse(
73+
[]string{"https://quay.io/cnr|community-operator|"},
74+
mfInput.csvSources,
75+
)
76+
assert.Error(t, err)
77+
}
1678
}

0 commit comments

Comments
 (0)
Please sign in to comment.