diff --git a/pkg/oc/cli/admin/release/coreos.go b/pkg/oc/cli/admin/release/coreos.go new file mode 100644 index 000000000000..1c138f3da15d --- /dev/null +++ b/pkg/oc/cli/admin/release/coreos.go @@ -0,0 +1,105 @@ +package release + +// This package parses the HTTP API effectively +// created by https://github.com/coreos/coreos-assembler + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/pkg/errors" +) + +// BuildMeta is a partial deserialization of the `meta.json` generated +// by coreos-assembler for a build. +type BuildMeta struct { + AMIs []struct { + HVM string `json:"hvm"` + Name string `json:"name"` + } `json:"amis"` + BuildID string `json:"buildid"` + Images struct { + QEMU struct { + Path string `json:"path"` + SHA256 string `json:"sha256"` + } `json:"qemu"` + } `json:"images"` + OSTreeVersion string `json:"ostree-version"` + OSContainer struct { + Digest string `json:"digest"` + Image string `json:"image"` + } `json:"oscontainer"` +} + +// httpGetAll downloads a URL and gives you a byte array. +func httpGetAll(ctx context.Context, url string) ([]byte, error) { + var body []byte + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return body, errors.Wrap(err, "failed to build request") + } + + client := &http.Client{} + resp, err := client.Do(req.WithContext(ctx)) + if err != nil { + return body, errors.Wrapf(err, "failed to fetch %s", url) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return body, errors.Errorf("fetching %s status %s", url, resp.Status) + } + + body, err = ioutil.ReadAll(resp.Body) + if err != nil { + return body, errors.Wrap(err, "failed to read HTTP response") + } + + return body, nil +} + +// getLatestBuildVersion returns the latest CoreOS build version number +func getLatestBuildVersion(ctx context.Context, baseURL string) (string, error) { + var builds struct { + Builds []string `json:"builds"` + } + buildsBuf, err := httpGetAll(ctx, baseURL + "/builds.json") + if err != nil { + return "", err + } + if err := json.Unmarshal(buildsBuf, &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 +} + +// GetLatest returns the CoreOS build with target version. If version +// is the empty string, the latest will be used. +func GetCoreOSBuild(ctx context.Context, baseURL string, version string) (*BuildMeta, error) { + var err error + if version == "" { + version, err = getLatestBuildVersion(ctx, baseURL) + if err != nil { + return nil, err + } + } + buildUrl := fmt.Sprintf("%s/%s/meta.json", baseURL, version) + buildStr, err := httpGetAll(ctx, buildUrl) + if err != nil { + return nil, err + } + + var build BuildMeta + if err := json.Unmarshal(buildStr, &build); err != nil { + return nil, errors.Wrap(err, "failed to parse HTTP response") + } + return &build, nil +} diff --git a/pkg/oc/cli/admin/release/new.go b/pkg/oc/cli/admin/release/new.go index 0da12e993d77..629ef5121947 100644 --- a/pkg/oc/cli/admin/release/new.go +++ b/pkg/oc/cli/admin/release/new.go @@ -4,6 +4,7 @@ import ( "archive/tar" "bufio" "bytes" + "context" "compress/gzip" "encoding/json" "fmt" @@ -39,6 +40,12 @@ import ( "github.com/openshift/origin/pkg/oc/cli/image/extract" ) +const ( + // coreOSBootImageLabel contains JSON metadata from coreos-assembler. + // In the future it may be used by the installer. + coreOSBootImageLabel = "io.openshift.release.coreos-boot-image" +) + func NewNewOptions(streams genericclioptions.IOStreams) *NewOptions { return &NewOptions{ IOStreams: streams, @@ -102,6 +109,8 @@ func NewRelease(f kcmdutil.Factory, parentName string, streams genericclioptions flags.StringVar(&o.FromDirectory, "from-dir", o.FromDirectory, "Use this directory as the source for the release payload.") flags.StringVar(&o.FromReleaseImage, "from-release", o.FromReleaseImage, "Use an existing release image as input.") flags.StringVar(&o.ReferenceMode, "reference-mode", o.ReferenceMode, "By default, the image reference from an image stream points to the public registry for the stream and the image digest. Pass 'source' to build references to the originating image.") + flags.StringVar(&o.CoreOSURL, "coreos-url", o.CoreOSURL, "URL for CoreOS release server") + flags.StringVar(&o.CoreOSVersion, "coreos-version", o.CoreOSVersion, "Choose this CoreOS version instead of picking latest in the stream") // properties of the release flags.StringVar(&o.Name, "name", o.Name, "The name of the release. Will default to the current time.") @@ -148,6 +157,8 @@ type NewOptions struct { FromImageStream string Namespace string ReferenceMode string + CoreOSURL string + CoreOSVersion string Exclude []string AlwaysInclude []string @@ -538,6 +549,32 @@ func (o *NewOptions) Run() error { is.Annotations = make(map[string]string) } + if o.CoreOSURL != "" { + coreosBuild, err := GetCoreOSBuild(context.TODO(), o.CoreOSURL, o.CoreOSVersion) + if err != nil { + return err + } + fmt.Fprintf(o.Out, "Using CoreOS build %s\n", coreosBuild.BuildID) + digestedImage := fmt.Sprintf("%s@%s", coreosBuild.OSContainer.Image, coreosBuild.OSContainer.Digest) + if digestedImage == "@" { + return fmt.Errorf("No oscontainer in CoreOS build") + } + + // Hardcoded, this name was chosen in one of the machine-config-operator PRs + // and added to the release payload by Clayton. + o.Mappings = append(o.Mappings, Mapping{Source: "machine-os-content", + Destination: digestedImage}) + + // And inject the full build metadata - primarily useful for + // "bootimages" i.e. AMIs/qcow2/etc. + serializedBuild, err := json.Marshal(coreosBuild) + if err != nil { + return err + } + // This is written as a label in the final image + is.Annotations[coreOSBootImageLabel] = string(serializedBuild) + } + // update any custom mappings and then sort the spec tags for _, m := range o.Mappings { if exclude.Has(m.Source) { @@ -930,6 +967,8 @@ func (o *NewOptions) write(r io.Reader, is *imageapi.ImageStream, now time.Time) if len(dgst) > 0 { config.Config.Labels["io.openshift.release.base-image-digest"] = dgst.String() } + config.Config.Labels[coreOSBootImageLabel] = is.Annotations[coreOSBootImageLabel] + return nil }