Skip to content

Commit dc93a71

Browse files
committed
pkg/rhcos/marketplace: add azure marketplace
Adds the ability to discover existing images in the Azure marketplace to be used for populating the marketplace rhcos stream.
1 parent 233ea85 commit dc93a71

File tree

1 file changed

+288
-0
lines changed

1 file changed

+288
-0
lines changed
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
package azure
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
"strings"
8+
9+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
10+
"github.com/coreos/stream-metadata-go/stream/rhcos"
11+
"github.com/sirupsen/logrus"
12+
"golang.org/x/mod/semver"
13+
14+
azuresession "github.com/openshift/installer/pkg/asset/installconfig/azure"
15+
"github.com/openshift/installer/pkg/types/azure"
16+
)
17+
18+
const (
19+
// region is an arbitrarily chosen region. Marketplace
20+
// images are published globally, we just need to verify
21+
// the image exists, so we can use any region.
22+
23+
region = "centralus"
24+
25+
// image attributes for the NoPurchasePlan image,
26+
// published by ARO.
27+
pubARO = "azureopenshift"
28+
offerARO = "aro4"
29+
30+
// image attributes for paid marketplace images.
31+
pubRH = "redhat"
32+
pubEMEA = "redhat-limited"
33+
offerOCP = "rh-ocp-worker"
34+
offerOPP = "rh-opp-worker"
35+
offerOKE = "rh-oke-worker"
36+
37+
// supported architectures.
38+
x86 = "x86_64"
39+
arm64 = "aarch64"
40+
)
41+
42+
// MarketplaceStream connects to the Azure SDK to populate the RHCOS stream.
43+
type MarketplaceStream struct {
44+
client *armcompute.VirtualMachineImagesClient
45+
}
46+
47+
type imgsQuery struct {
48+
gen1, gen2 *imgQuery
49+
}
50+
51+
type imgQuery struct {
52+
publisher, offer, sku, xyVersion string
53+
}
54+
55+
// NewStreamClient instantiates a MarketplaceStream.
56+
func NewStreamClient() (*MarketplaceStream, error) {
57+
cl, err := getClient()
58+
if err != nil {
59+
return nil, fmt.Errorf("failed to create azure marketplace stream client: %w", err)
60+
}
61+
return &MarketplaceStream{cl}, nil
62+
}
63+
64+
// Populate finds the marketplace images for a given architecture and release.
65+
func (az *MarketplaceStream) Populate(ctx context.Context, arch, rel string) (*rhcos.AzureMarketplace, error) {
66+
s := &rhcos.AzureMarketplace{}
67+
68+
var err error
69+
if s.NoPurchasePlan, err = az.noPurchasePlan(ctx, arch, rel); err != nil {
70+
return nil, fmt.Errorf("failed getting Azure non-paid images: %w", err)
71+
}
72+
73+
if s.OCP, err = az.getImages(ctx, paidImageQuery(pubRH, rel, offerOCP), arch); err != nil {
74+
return nil, fmt.Errorf("failed getting Azure OCP marketplace images: %w", err)
75+
}
76+
77+
if s.OPP, err = az.getImages(ctx, paidImageQuery(pubRH, rel, offerOPP), arch); err != nil {
78+
return nil, fmt.Errorf("failed getting Azure OPP marketplace images: %w", err)
79+
}
80+
81+
if s.OKE, err = az.getImages(ctx, paidImageQuery(pubRH, rel, offerOKE), arch); err != nil {
82+
return nil, fmt.Errorf("failed getting Azure OKE marketplace images: %w", err)
83+
}
84+
85+
if s.OCPEMEA, err = az.getImages(ctx, paidImageQuery(pubEMEA, rel, offerOCP), arch); err != nil {
86+
return nil, fmt.Errorf("failed getting Azure OCP EMEA marketplace images: %w", err)
87+
}
88+
89+
if s.OPPEMEA, err = az.getImages(ctx, paidImageQuery(pubEMEA, rel, offerOPP), arch); err != nil {
90+
return nil, fmt.Errorf("failed getting Azure OPP EMEA marketplace images: %w", err)
91+
}
92+
93+
if s.OKEEMEA, err = az.getImages(ctx, paidImageQuery(pubEMEA, rel, offerOKE), arch); err != nil {
94+
return nil, fmt.Errorf("failed getting Azure OKE EMEA marketplace images: %w", err)
95+
}
96+
97+
return s, nil
98+
}
99+
100+
func (az *MarketplaceStream) noPurchasePlan(ctx context.Context, arch, release string) (*rhcos.AzureMarketplaceImages, error) {
101+
logrus.Info("Retrieving NoPurchase Plan Images for release: ", release)
102+
gen1SKU, gen2SKU := parseAROSKUs(release, arch)
103+
q := imgsQuery{
104+
gen1: &imgQuery{
105+
publisher: pubARO,
106+
offer: offerARO,
107+
sku: gen1SKU,
108+
xyVersion: release,
109+
},
110+
gen2: &imgQuery{
111+
publisher: pubARO,
112+
offer: offerARO,
113+
sku: gen2SKU,
114+
xyVersion: release,
115+
},
116+
}
117+
return az.getImages(ctx, q, arch)
118+
}
119+
120+
func paidImageQuery(pub, release, offer string) imgsQuery {
121+
return imgsQuery{
122+
gen1: &imgQuery{
123+
publisher: pub,
124+
offer: offer,
125+
sku: fmt.Sprintf("%s-gen1", offer),
126+
xyVersion: release,
127+
},
128+
gen2: &imgQuery{
129+
publisher: pub,
130+
offer: offer,
131+
sku: offer,
132+
xyVersion: release,
133+
},
134+
}
135+
}
136+
137+
func (az *MarketplaceStream) getImages(ctx context.Context, query imgsQuery, arch string) (*rhcos.AzureMarketplaceImages, error) {
138+
imgs := &rhcos.AzureMarketplaceImages{}
139+
if gen1 := query.gen1; gen1 != nil && gen1.sku != "" {
140+
logrus.Infof("Searching for image with publisher: %s, offer %s, sku %s architecture %s in release %s", gen1.publisher, gen1.offer, gen1.sku, arch, gen1.xyVersion)
141+
img, err := az.getImage(ctx, gen1.publisher, gen1.offer, gen1.sku, gen1.xyVersion, arch)
142+
if err != nil {
143+
logrus.Error(err)
144+
}
145+
imgs.Gen1 = img
146+
}
147+
if gen2 := query.gen2; gen2 != nil && gen2.sku != "" {
148+
logrus.Infof("Searching for image with publisher: %s, offer %s, sku %s architecture %s in release %s", gen2.publisher, gen2.offer, gen2.sku, arch, gen2.xyVersion)
149+
img, err := az.getImage(ctx, gen2.publisher, gen2.offer, gen2.sku, gen2.xyVersion, arch)
150+
if err != nil {
151+
logrus.Error(err)
152+
}
153+
imgs.Gen2 = img
154+
}
155+
if imgs.Gen1 == nil && imgs.Gen2 == nil {
156+
return nil, nil
157+
}
158+
return imgs, nil
159+
}
160+
161+
// getImage finds the latest version matching the x.y version of the release.
162+
func (az *MarketplaceStream) getImage(ctx context.Context, pub, offer, sku, xyVersion, arch string) (*rhcos.AzureMarketplaceImage, error) {
163+
resp, err := az.client.List(ctx, region, pub, offer, sku, nil)
164+
if err != nil {
165+
return nil, fmt.Errorf("failed to list images: %w", err)
166+
}
167+
168+
if len(resp.VirtualMachineImageResourceArray) == 0 {
169+
logrus.Infof("Found no images for publisher: %s, offer: %s, sku %s for the architecture %s in the release %s", pub, offer, sku, arch, xyVersion)
170+
return nil, nil
171+
}
172+
173+
var foundVersion string
174+
var greatestSemver string
175+
for _, v := range resp.VirtualMachineImageResourceArray {
176+
v := *v.Name
177+
semVer := convertToSemver(v)
178+
logrus.Infof("Found potential image match, version: %s", v)
179+
180+
// Ensure that the image is not from a later Y stream,
181+
// e.g. if we are populating a 4.19 stream, we don't want 4.20 images,
182+
// but 4.18 would be ok if 4.19 is not available yet.
183+
if checkIfNewer(semVer, xyVersion) {
184+
logrus.Infof("Skipping version %s as it is released after %s", v, xyVersion)
185+
continue
186+
}
187+
188+
if semver.Compare(greatestSemver, semVer) < 0 {
189+
greatestSemver = semVer
190+
foundVersion = v
191+
}
192+
}
193+
194+
// Now that we've found the version, check the architecture and the plan.
195+
img, err := az.client.Get(ctx, region, pub, offer, sku, foundVersion, nil)
196+
if err != nil {
197+
return nil, fmt.Errorf("could not get the image for the found version, urn: %s:%s:%s:%s in region %s: %w", pub, offer, sku, foundVersion, region, err)
198+
}
199+
200+
// This way of checking architecture is works,
201+
// but may be unnecessary. We would only need to do something
202+
// like this if the URN for different architectures can be the same;
203+
// otherwise we know before generating the query which architecture we are looking for.
204+
azureArch := map[string]armcompute.ArchitectureTypes{
205+
x86: armcompute.ArchitectureTypesX64,
206+
arm64: armcompute.ArchitectureTypesArm64,
207+
}
208+
209+
if *img.Properties.Architecture != azureArch[arch] {
210+
return nil, nil
211+
}
212+
213+
logrus.Infof("Using image %s:%s:%s:%s", pub, offer, sku, foundVersion)
214+
return &rhcos.AzureMarketplaceImage{
215+
Publisher: pub,
216+
Offer: offer,
217+
SKU: sku,
218+
Version: foundVersion,
219+
}, nil
220+
}
221+
222+
// parseARO takes the release from coreos stream and
223+
// uses conventions to generate the SKU (gen1 & gen2) and version.
224+
// For instance, with a coreos release of "4.19"
225+
// gen1SKU: "aro_418"
226+
// gen2SKU: "418-v2"
227+
// version: "418.94.20241009" (removes timestamp & build number)
228+
func parseAROSKUs(release, arch string) (string, string) {
229+
xyVersion := strings.ReplaceAll(release, ".", "")
230+
var gen1SKU, gen2SKU string
231+
switch arch {
232+
case x86:
233+
gen1SKU = fmt.Sprintf("aro_%s", xyVersion)
234+
gen2SKU = fmt.Sprintf("%s-v2", xyVersion)
235+
case arm64:
236+
gen1SKU = ""
237+
gen2SKU = fmt.Sprintf("%s-arm", xyVersion)
238+
}
239+
return gen1SKU, gen2SKU
240+
}
241+
242+
func getClient() (*armcompute.VirtualMachineImagesClient, error) {
243+
ssn, err := azuresession.GetSession(azure.PublicCloud, "")
244+
if err != nil {
245+
return nil, fmt.Errorf("failed to get session: %w", err)
246+
}
247+
248+
client, err := armcompute.NewVirtualMachineImagesClient(ssn.Credentials.SubscriptionID, ssn.TokenCreds, nil)
249+
if err != nil {
250+
return nil, fmt.Errorf("failed to create client: %w", err)
251+
}
252+
return client, nil
253+
}
254+
255+
// ARO & the paid marketplace images format their version strings differently.
256+
// This function takes either one, and converts it to a go semantic version string.
257+
// ARO combines xy into what should be the x, and includes the RHEL version in y; e.g. 418.94.20250122.
258+
// Paid marketplace images use a correct semantic version (well, they use a timestamp for z, but good enough): 4.17.2024101109.
259+
func convertToSemver(ver string) string {
260+
// Using RHEL versioning
261+
if major := strings.Split(ver, ".")[0]; major == "9" || major == "10" {
262+
return fmt.Sprintf("v%s", ver)
263+
}
264+
265+
if segments := strings.Split(ver, "."); len(segments[0]) == 1 {
266+
semV := fmt.Sprintf("v%s", ver)
267+
return semV
268+
} else if len(segments[0]) == 3 {
269+
combinedXY := segments[0]
270+
semV := fmt.Sprintf("v%s", strings.Join([]string{combinedXY[:1], combinedXY[1:], segments[2]}, "."))
271+
return semV
272+
}
273+
return ""
274+
}
275+
276+
func checkIfNewer(candidate, release string) bool {
277+
img, err := strconv.Atoi(strings.Split(semver.MajorMinor(candidate), ".")[1])
278+
if err != nil {
279+
logrus.Infof("Error converting minor version to int with version %s", candidate)
280+
return true
281+
}
282+
rel, err := strconv.Atoi(strings.Split(release, ".")[1])
283+
if err != nil {
284+
logrus.Infof("Error converting minor version to int with version %s", release)
285+
return true
286+
}
287+
return img > rel
288+
}

0 commit comments

Comments
 (0)