Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rhcos: implement image discovery for new pipeline #554

Merged
merged 1 commit into from
Oct 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions pkg/asset/installconfig/platform.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package installconfig

import (
"context"
"encoding/json"
"fmt"
"net/url"
Expand All @@ -12,6 +13,7 @@ import (
survey "gopkg.in/AlecAivazis/survey.v1"

"github.com/openshift/installer/pkg/asset"
"github.com/openshift/installer/pkg/rhcos"
"github.com/openshift/installer/pkg/types"
)

Expand Down Expand Up @@ -52,7 +54,6 @@ var (

defaultLibvirtNetworkIfName = "tt0"
defaultLibvirtNetworkIPRange = "192.168.126.0/24"
defaultLibvirtImageURL = "http://aos-ostree.rhev-ci-vms.eng.rdu2.redhat.com/rhcos/images/cloud/latest/rhcos-qemu.qcow2.gz"
)

// Platform is an asset that queries the user for the platform on which to install
Expand Down Expand Up @@ -288,13 +289,24 @@ func (a *platform) libvirtPlatform() (*types.LibvirtPlatform, error) {
return nil, err
}

// TODO: Ideally, this would live inside of a closure which is passed to
// asset.GenerateUserProvidedAsset and only called if the environment
// variable isn't present. As this exists, it ruins the abstraction.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sort of problem goes away with #556, where all dependencies and defaults are only rendered as needed 🎉

var qcowImage string
if _, ok := os.LookupEnv("OPENSHIFT_INSTALL_LIBVIRT_IMAGE"); !ok {
qcowImage, err = rhcos.QEMU(context.TODO(), rhcos.DefaultChannel)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch QEMU image URL")
}
}

image, err := asset.GenerateUserProvidedAsset(
"Libvirt Image",
&survey.Question{
Prompt: &survey.Input{
Message: "Image",
Help: "URI of the OS image.",
Default: defaultLibvirtImageURL,
Default: qcowImage,
},
Validate: survey.ComposeValidators(survey.Required, uriValidator),
},
Expand Down
80 changes: 8 additions & 72 deletions pkg/rhcos/ami.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,86 +2,22 @@ package rhcos

import (
"context"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/pkg/errors"
)

const (
// DefaultChannel is the default RHCOS channel for the cluster.
DefaultChannel = "tested"
)

// AMI calculates a Red Hat CoreOS AMI.
func AMI(ctx context.Context, channel, region string) (ami string, err error) {
if channel != DefaultChannel {
return "", errors.Errorf("channel %q is not yet supported", channel)
}

ssn := session.Must(session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
Config: aws.Config{
Region: aws.String(region),
},
}))

svc := ec2.New(ssn)

result, err := svc.DescribeImagesWithContext(ctx, &ec2.DescribeImagesInput{
Filters: []*ec2.Filter{
{
Name: aws.String("name"),
Values: aws.StringSlice([]string{"redhat-coreos-*"}),
},
{
Name: aws.String("architecture"),
Values: aws.StringSlice([]string{"x86_64"}),
},
{
Name: aws.String("virtualization-type"),
Values: aws.StringSlice([]string{"hvm"}),
},
{
Name: aws.String("image-type"),
Values: aws.StringSlice([]string{"machine"}),
},
{
Name: aws.String("owner-id"),
Values: aws.StringSlice([]string{"531415883065"}),
},
{
Name: aws.String("state"),
Values: aws.StringSlice([]string{"available"}),
},
},
})
// AMI fetches the HVM AMI ID of the latest Red Hat CoreOS release.
func AMI(ctx context.Context, channel, region string) (string, error) {
meta, err := fetchLatestMetadata(ctx, channel)
if err != nil {
return "", errors.Wrap(err, "failed to describe AMIs")
return "", errors.Wrap(err, "failed to fetch RHCOS metadata")
}

var image *ec2.Image
var created time.Time
for _, nextImage := range result.Images {
if nextImage.ImageId == nil || nextImage.CreationDate == nil {
continue
for _, ami := range meta.AMIs {
if ami.Name == region {
Copy link
Member

@wking wking Oct 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to land this before the AMIs are being pushed to other regions?

$ BUILD=$(curl -sL https://releases-rhcos.svc.ci.openshift.org/storage/releases/maipo/builds.json | jq -r '.builds[0]')
$ echo "${BUILD}"
47.38
$ curl -sL "https://releases-rhcos.svc.ci.openshift.org/storage/releases/maipo/${BUILD}/meta.json" | jq .amis
[
  {
    "name": "us-east-1",
    "hvm": "ami-03afbcc8b72560ac8"
  }
]

I'd rather stick to scraping AWS until we get that, because scraping AWS has a chance at working in other regions if folks can copy the AMI over to the target region. That would allow us to use the other regions we're trying to use for openshift-dev.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add an environment variable for the AMI. We already have the override for the QEMU image.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add an environment variable for the AMI. We already have the override for the QEMU image.

I'm ok with that, although see this comment about not having AMI/image knobs and assuming that there's a default AMI/image that is (1) available and (2) healthy enough to pivot.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're close to landing the multi-region again although this is going to slow our pipeline notably. There are two things we need to do; first on the RHCOS side, implement tags, and only upload to all the regions for tags (e.g. once a day at most?).

Second though we need to do openshift/os#289 or possibly teach the installer to take a set of override RPMs and apply them via Ignition? Then I think we can slow the cadence of "promoted" builds, and anyone who wants to test bleeding edge OS work can use libvirt.

return ami.HVM, nil
}
nextCreated, err := time.Parse(time.RFC3339, *nextImage.CreationDate)
if err != nil {
return "", errors.Wrap(err, "failed to parse AMIs CreationDate to time.RFC3339")
}

if image == nil || nextCreated.After(created) {
image = nextImage
created = nextCreated
}
}

if image == nil {
return "", errors.Errorf("no RHCOS AMIs found in %s", region)
}

return *image.ImageId, nil
return "", errors.Errorf("no RHCOS AMIs found in %s", region)
}
108 changes: 108 additions & 0 deletions pkg/rhcos/builds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package rhcos

import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"

"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

const (
// DefaultChannel is the default RHCOS channel for the cluster.
DefaultChannel = "maipo"

baseURL = "https://releases-rhcos.svc.ci.openshift.org/storage/releases"
)

type metadata struct {
AMIs []struct {
HVM string `json:"hvm"`
Name string `json:"name"`
} `json:"amis"`
Images struct {
QEMU struct {
Path string `json:"path"`
SHA256 string `json:"sha256"`
} `json:"qemu"`
} `json:"images"`
OSTreeVersion string `json:"ostree-version"`
}

func fetchLatestMetadata(ctx context.Context, channel string) (metadata, error) {
build, err := fetchLatestBuild(ctx, channel)
if err != nil {
return metadata{}, errors.Wrap(err, "failed to fetch latest build")
}

url := fmt.Sprintf("%s/%s/%s/meta.json", baseURL, channel, build)
logrus.Debugf("Fetching RHCOS metadata from %q", url)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return metadata{}, errors.Wrap(err, "failed to build request")
}

client := &http.Client{}
resp, err := client.Do(req.WithContext(ctx))
if err != nil {
return metadata{}, errors.Wrap(err, "failed to fetch metadata")
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return metadata{}, errors.Errorf("incorrect HTTP response (%s)", resp.Status)
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return metadata{}, errors.Wrap(err, "failed to read HTTP response")
}

var meta metadata
if err := json.Unmarshal(body, &meta); err != nil {
return meta, errors.Wrap(err, "failed to parse HTTP response")
}

return meta, nil
}

func fetchLatestBuild(ctx context.Context, channel string) (string, error) {
url := fmt.Sprintf("%s/%s/builds.json", baseURL, channel)
logrus.Debugf("Fetching RHCOS builds from %q", url)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", errors.Wrap(err, "failed to build request")
}

client := &http.Client{}
resp, err := client.Do(req.WithContext(ctx))
if err != nil {
return "", errors.Wrap(err, "failed to fetch builds")
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", errors.Errorf("incorrect HTTP response (%s)", resp.Status)
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", errors.Wrap(err, "failed to read HTTP response")
}

var builds struct {
Builds []string `json:"builds"`
}
if err := json.Unmarshal(body, &builds); err != nil {
return "", errors.Wrap(err, "failed to parse HTTP response")
}

if len(builds.Builds) == 0 {
return "", errors.Errorf("no builds found")
}

return builds.Builds[0], nil
}
18 changes: 18 additions & 0 deletions pkg/rhcos/qemu.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package rhcos

import (
"context"
"fmt"

"github.com/pkg/errors"
)

// QEMU fetches the URL of the latest Red Hat CoreOS release.
func QEMU(ctx context.Context, channel string) (string, error) {
meta, err := fetchLatestMetadata(ctx, channel)
if err != nil {
return "", errors.Wrap(err, "failed to fetch RHCOS metadata")
}

return fmt.Sprintf("%s/%s/%s/%s", baseURL, channel, meta.OSTreeVersion, meta.Images.QEMU.Path), nil
}