From 2561c600e885aa6894e7900d5758f0c5a1ea6066 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 21 Feb 2019 04:15:15 -0800 Subject: [PATCH] WIP: pkg/rhcos/updatepayload: Extract RHCOS build from update payload Since e2b31b22 (bootkube: Supply machine-os-content to MCO, 2019-01-29, #1149), we have been using the machine-os-content image to seed the machine-config operator. With this commit, use the RHCOS build ID from that image's annotations to calculate our AMI, etc. as well. This gives one less degree of freedom for breaking things ;). Users who want to test clusters based on a different RHCOS build should bump the value in their update payload, just like users testing operator updates and other changes. This is a WIP until I look into signature verification. --- hack/build.sh | 6 +- pkg/asset/ignition/bootstrap/bootstrap.go | 21 +-- pkg/asset/release/release.go | 41 ++++++ pkg/asset/rhcos/image.go | 21 ++- pkg/rhcos/ami.go | 4 +- pkg/rhcos/builds.go | 53 +------- pkg/rhcos/qemu.go | 4 +- pkg/rhcos/updatepayload/updatepayload.go | 157 ++++++++++++++++++++++ 8 files changed, 226 insertions(+), 81 deletions(-) create mode 100644 pkg/asset/release/release.go create mode 100644 pkg/rhcos/updatepayload/updatepayload.go diff --git a/hack/build.sh b/hack/build.sh index a68c93d596d..05650ebb9e2 100755 --- a/hack/build.sh +++ b/hack/build.sh @@ -42,11 +42,7 @@ release) TAGS="${TAGS} release" if test -n "${RELEASE_IMAGE}" then - LDFLAGS="${LDFLAGS} -X github.com/openshift/installer/pkg/asset/ignition/bootstrap.defaultReleaseImage=${RELEASE_IMAGE}" - fi - if test -n "${RHCOS_BUILD_NAME}" - then - LDFLAGS="${LDFLAGS} -X github.com/openshift/installer/pkg/rhcos.buildName=${RHCOS_BUILD_NAME}" + LDFLAGS="${LDFLAGS} -X github.com/openshift/installer/pkg/asset/release.defaultReleaseImage=${RELEASE_IMAGE}" fi if test "${SKIP_GENERATION}" != y then diff --git a/pkg/asset/ignition/bootstrap/bootstrap.go b/pkg/asset/ignition/bootstrap/bootstrap.go index fcc1ff25001..a77af28cdc4 100644 --- a/pkg/asset/ignition/bootstrap/bootstrap.go +++ b/pkg/asset/ignition/bootstrap/bootstrap.go @@ -24,6 +24,7 @@ import ( "github.com/openshift/installer/pkg/asset/kubeconfig" "github.com/openshift/installer/pkg/asset/machines" "github.com/openshift/installer/pkg/asset/manifests" + "github.com/openshift/installer/pkg/asset/release" "github.com/openshift/installer/pkg/asset/tls" "github.com/openshift/installer/pkg/types" ) @@ -36,10 +37,6 @@ const ( ignitionUser = "core" ) -var ( - defaultReleaseImage = "registry.svc.ci.openshift.org/openshift/origin-release:v4.0" -) - // bootstrapTemplateData is the data to use to replace values in bootstrap // template files. type bootstrapTemplateData struct { @@ -62,6 +59,7 @@ var _ asset.WritableAsset = (*Bootstrap)(nil) func (a *Bootstrap) Dependencies() []asset.Asset { return []asset.Asset{ &installconfig.InstallConfig{}, + new(release.Release), &tls.RootCA{}, &tls.EtcdCA{}, &tls.KubeCA{}, @@ -85,9 +83,10 @@ func (a *Bootstrap) Dependencies() []asset.Asset { // Generate generates the ignition config for the Bootstrap asset. func (a *Bootstrap) Generate(dependencies asset.Parents) error { installConfig := &installconfig.InstallConfig{} - dependencies.Get(installConfig) + release := new(release.Release) + dependencies.Get(installConfig, release) - templateData, err := a.getTemplateData(installConfig.Config) + templateData, err := a.getTemplateData(installConfig.Config, string(*release)) if err != nil { return errors.Wrap(err, "failed to get bootstrap templates") } @@ -139,23 +138,17 @@ func (a *Bootstrap) Files() []*asset.File { } // getTemplateData returns the data to use to execute bootstrap templates. -func (a *Bootstrap) getTemplateData(installConfig *types.InstallConfig) (*bootstrapTemplateData, error) { +func (a *Bootstrap) getTemplateData(installConfig *types.InstallConfig, release string) (*bootstrapTemplateData, error) { etcdEndpoints := make([]string, *installConfig.ControlPlane.Replicas) for i := range etcdEndpoints { etcdEndpoints[i] = fmt.Sprintf("https://etcd-%d.%s:2379", i, installConfig.ClusterDomain()) } - releaseImage := defaultReleaseImage - if ri, ok := os.LookupEnv("OPENSHIFT_INSTALL_RELEASE_IMAGE_OVERRIDE"); ok && ri != "" { - logrus.Warn("Found override for ReleaseImage. Please be warned, this is not advised") - releaseImage = ri - } - return &bootstrapTemplateData{ EtcdCertSignerImage: etcdCertSignerImage, EtcdctlImage: etcdctlImage, PullSecret: installConfig.PullSecret, - ReleaseImage: releaseImage, + ReleaseImage: release, EtcdCluster: strings.Join(etcdEndpoints, ","), }, nil } diff --git a/pkg/asset/release/release.go b/pkg/asset/release/release.go new file mode 100644 index 00000000000..1ff0a483a6a --- /dev/null +++ b/pkg/asset/release/release.go @@ -0,0 +1,41 @@ +// Package release contains assets for the release image (also known +// as the update payload). +package release + +import ( + "os" + + "github.com/sirupsen/logrus" + + "github.com/openshift/installer/pkg/asset" +) + +var ( + defaultReleaseImage = "registry.svc.ci.openshift.org/openshift/origin-release:v4.0" +) + +// Release is the pull-spec for the release image. +type Release string + +var _ asset.Asset = (*Release)(nil) + +// Name returns the human-friendly name of the asset. +func (i *Release) Name() string { + return "Release" +} + +// Dependencies returns no dependencies. +func (i *Release) Dependencies() []asset.Asset { + return nil +} + +// Generate the release image. +func (r *Release) Generate(p asset.Parents) error { + releaseImage := defaultReleaseImage + if ri := os.Getenv("OPENSHIFT_INSTALL_RELEASE_IMAGE_OVERRIDE"); ri != "" { + logrus.Warn("Found override for ReleaseImage. Please be warned, this is not advised") + releaseImage = ri + } + *r = Release(releaseImage) + return nil +} diff --git a/pkg/asset/rhcos/image.go b/pkg/asset/rhcos/image.go index 956b85bc320..f0bc76b84d0 100644 --- a/pkg/asset/rhcos/image.go +++ b/pkg/asset/rhcos/image.go @@ -11,7 +11,9 @@ import ( "github.com/openshift/installer/pkg/asset" "github.com/openshift/installer/pkg/asset/installconfig" + "github.com/openshift/installer/pkg/asset/release" "github.com/openshift/installer/pkg/rhcos" + "github.com/openshift/installer/pkg/rhcos/updatepayload" "github.com/openshift/installer/pkg/types/aws" "github.com/openshift/installer/pkg/types/libvirt" "github.com/openshift/installer/pkg/types/none" @@ -30,10 +32,11 @@ func (i *Image) Name() string { return "Image" } -// Dependencies returns no dependencies. +// Dependencies returns the assets on which the Image asset depends. func (i *Image) Dependencies() []asset.Asset { return []asset.Asset{ &installconfig.InstallConfig{}, + new(release.Release), } } @@ -46,18 +49,24 @@ func (i *Image) Generate(p asset.Parents) error { } ic := &installconfig.InstallConfig{} - p.Get(ic) + release := new(release.Release) + p.Get(ic, release) config := ic.Config + ctx := context.TODO() + build, err := updatepayload.RHCOSBuild(ctx, string(*release)) + if err != nil { + return err + } + var osimage string - var err error - ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second) + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() switch config.Platform.Name() { case aws.Name: - osimage, err = rhcos.AMI(ctx, rhcos.DefaultChannel, config.Platform.AWS.Region) + osimage, err = rhcos.AMI(ctx, rhcos.DefaultChannel, build, config.Platform.AWS.Region) case libvirt.Name: - osimage, err = rhcos.QEMU(ctx, rhcos.DefaultChannel) + osimage, err = rhcos.QEMU(ctx, rhcos.DefaultChannel, build) case openstack.Name: osimage = "rhcos" case none.Name: diff --git a/pkg/rhcos/ami.go b/pkg/rhcos/ami.go index 2726e4c6fcd..3c18f24bb20 100644 --- a/pkg/rhcos/ami.go +++ b/pkg/rhcos/ami.go @@ -7,8 +7,8 @@ import ( ) // 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) +func AMI(ctx context.Context, channel, build, region string) (string, error) { + meta, err := fetchMetadata(ctx, channel, build) if err != nil { return "", errors.Wrap(err, "failed to fetch RHCOS metadata") } diff --git a/pkg/rhcos/builds.go b/pkg/rhcos/builds.go index 015b50ef615..31f26c28d8c 100644 --- a/pkg/rhcos/builds.go +++ b/pkg/rhcos/builds.go @@ -15,10 +15,6 @@ var ( // DefaultChannel is the default RHCOS channel for the cluster. DefaultChannel = "maipo" - // buildName is the name of the build in the channel that will be picked up - // empty string means the first one in the build list (latest) will be used - buildName = "" - baseURL = "https://releases-rhcos.svc.ci.openshift.org/storage/releases" ) @@ -36,16 +32,7 @@ type metadata struct { OSTreeVersion string `json:"ostree-version"` } -func fetchLatestMetadata(ctx context.Context, channel string) (metadata, error) { - build := buildName - var err error - if build == "" { - build, err = fetchLatestBuild(ctx, channel) - if err != nil { - return metadata{}, errors.Wrap(err, "failed to fetch latest build") - } - } - +func fetchMetadata(ctx context.Context, channel string, build string) (metadata, error) { 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) @@ -76,41 +63,3 @@ func fetchLatestMetadata(ctx context.Context, channel string) (metadata, error) 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 -} diff --git a/pkg/rhcos/qemu.go b/pkg/rhcos/qemu.go index 8fd30de8c91..e06d24456b0 100644 --- a/pkg/rhcos/qemu.go +++ b/pkg/rhcos/qemu.go @@ -8,8 +8,8 @@ import ( ) // 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) +func QEMU(ctx context.Context, channel string, build string) (string, error) { + meta, err := fetchMetadata(ctx, channel, build) if err != nil { return "", errors.Wrap(err, "failed to fetch RHCOS metadata") } diff --git a/pkg/rhcos/updatepayload/updatepayload.go b/pkg/rhcos/updatepayload/updatepayload.go new file mode 100644 index 00000000000..47acdd42b7b --- /dev/null +++ b/pkg/rhcos/updatepayload/updatepayload.go @@ -0,0 +1,157 @@ +// Package updatepayload extracts the RHCOS build version from an +// OpenShift update payload. +package updatepayload + +import ( + "archive/tar" + "compress/gzip" + "context" + "io" + "io/ioutil" + + "github.com/containers/image/docker" + "github.com/containers/image/image" + "github.com/containers/image/pkg/blobinfocache" + "github.com/containers/image/transports" + "github.com/containers/image/types" + "github.com/docker/distribution/reference" + imagev1 "github.com/openshift/api/image/v1" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" +) + +func pull(ctx context.Context, pullSpec string) (types.ImageSource, types.Image, error) { + named, err := reference.ParseNamed(pullSpec) + if err != nil { + return nil, nil, err + } + + reference, err := docker.NewReference(named) + if err != nil { + return nil, nil, err + } + + sys := &types.SystemContext{} + imageSource, err := reference.NewImageSource(ctx, sys) + if err != nil { + return nil, nil, err + } + defer func() { + if err != nil { + imageSource.Close() + } + }() + + unparsed := image.UnparsedInstance(imageSource, nil) + // TODO: signature validation + + img, err := image.FromUnparsedImage(ctx, sys, unparsed) + return imageSource, img, err +} + +func imageReferences(ctx context.Context, updatePayloadPullSpec string) ([]byte, error) { + imageSource, image, err := pull(ctx, updatePayloadPullSpec) + if err != nil { + return nil, err + } + defer imageSource.Close() + + layers := image.LayerInfos() + for i := len(layers) - 1; i >= 0; i-- { + layer := layers[i] + blob, _, err := imageSource.GetBlob(ctx, layer, blobinfocache.NoCache) + if err != nil { + return nil, err + } + defer blob.Close() + + // fmt.Println(layer.MediaType) // TODO: blank from quay? Just assume this is a gzipped tar? + gzipReader, err := gzip.NewReader(blob) + if err != nil { + return nil, err + } + + tarReader := tar.NewReader(gzipReader) + for { + hdr, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if hdr.Name == "release-manifests/image-references" { + return ioutil.ReadAll(tarReader) + } + } + } + + return nil, errors.Errorf("no image reference found in %s", transports.ImageName(image.Reference())) +} + +func osContentPullSpec(imageReferences []byte) (string, error) { + scheme := runtime.NewScheme() + imagev1.Install(scheme) + decoder := serializer.NewCodecFactory(scheme).UniversalDecoder( + imagev1.GroupVersion, + ) + obj, _, err := decoder.Decode(imageReferences, nil, nil) + if err != nil { + return "", err + } + + imageStream, ok := obj.(*imagev1.ImageStream) + if !ok { + return "", errors.Errorf("image references is a %q, not an image stream", obj.GetObjectKind()) + } + + for _, tag := range imageStream.Spec.Tags { + if tag.Name == "machine-os-content" { + if tag.From == nil { + return "", errors.New("machine-os-content tag has an empty 'from' property") + } + + if tag.From.Kind != "DockerImage" { + return "", errors.Errorf("unrecognized machine-os-content tag from kind %q", tag.From.Kind) + } + + return tag.From.Name, nil + } + } + + return "", errors.New("no machine-os-content tag found") +} + +// RHCOSBuild extracts the RHCOS build version from an OpenShift +// update payload. +func RHCOSBuild(ctx context.Context, updatePayloadPullSpec string) (string, error) { + imageReferencesBytes, err := imageReferences(ctx, updatePayloadPullSpec) + if err != nil { + return "", err + } + + pullSpec, err := osContentPullSpec(imageReferencesBytes) + if err != nil { + return "", err + } + + imageSource, image, err := pull(ctx, pullSpec) + if err != nil { + return "", err + } + defer imageSource.Close() + + inspect, err := image.Inspect(ctx) + if err != nil { + return "", err + } + + version, ok := inspect.Labels["version"] + if !ok { + return "", errors.Errorf("%s has no version annotation", pullSpec) + } + + return version, nil +}