blob: ac2255128b573427a8a619ed56ac2e2144217bd3 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"encoding/json"
"fmt"
"time"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"infra/cmd/cloudbuildhelper/registry"
"infra/cmd/cloudbuildhelper/storage"
)
// See cmdBuild and cmdUpload help strings.
const inputsHashCanonicalTag = ":inputs-hash"
const (
imageRefMetaKey = "cbh-image-ref" // GCS metadata key for imageRef{...} JSON blobs
buildRefMetaKey = "cbh-build-ref" // GCS metadata key for buildRef{...} JSON blobs
)
// imageRef is stored as metadata of context tarballs in Google Storage.
//
// It refers to some image built from the tarball.
type imageRef struct {
Image string `json:"image"` // name of the uploaded image "<registry>/<name>"
Digest string `json:"digest"` // docker digest of the uploaded image "sha256:..."
CanonicalTag string `json:"tag"` // its canonical tag
BuildID string `json:"build_id,omitempty"` // parent CI build that produced this image (FYI)
Timestamp time.Time `json:"-"` // timestamp of the metadata entry
}
// buildRef is stored as metadata of context tarballs in Google Storage.
//
// If refers to some CI build that reused the tarball or image built from it.
// This information is retained for debugging.
type buildRef struct {
BuildID string `json:"build_id"` // value of -build-id flag (may be "")
CanonicalTag string `json:"tag,omitempty"` // value of -canonical-tag flag
Timestamp time.Time `json:"-"` // timestamp of the metadata entry
}
// Log dumps information about the image to the log.
func (r *imageRef) Log(ctx context.Context, preamble string) {
logging.Infof(ctx, "%s", preamble)
if r.CanonicalTag == "" {
logging.Infof(ctx, " Name: %s", r.Image)
} else {
logging.Infof(ctx, " Name: %s:%s", r.Image, r.CanonicalTag)
}
logging.Infof(ctx, " Digest: %s", r.Digest)
logging.Infof(ctx, " View: %s", r.ViewURL())
}
// ViewURL returns an URL of the image, for humans.
func (r *imageRef) ViewURL() string {
if r.CanonicalTag != "" {
return fmt.Sprintf("https://%s:%s", r.Image, r.CanonicalTag)
}
return fmt.Sprintf("https://%s@%s", r.Image, r.Digest)
}
// validateCanonicalTag returns a CLI error if the canonical tag is invalid.
func validateCanonicalTag(tag string) error {
if tag == "" {
return errBadFlag("-canonical-tag", "a value is required")
}
if tag != inputsHashCanonicalTag {
if err := registry.ValidateTag(tag); err != nil {
return errBadFlag("-canonical-tag", err.Error())
}
}
return nil
}
// validateTags returns a CLI error if some of the tags are invalid.
func validateTags(tags []string) error {
for _, t := range tags {
if err := registry.ValidateTag(t); err != nil {
return errBadFlag("-tag", err.Error())
}
}
return nil
}
// calcInputsHashCanonicalTag returns the actual value to use for :inputs-hash.
func calcInputsHashCanonicalTag(digest string) string {
return "cbh-inputs-" + digest[:24]
}
// updateMetadata appends to the metadata of the tarball in the storage.
//
// Adds serialized 'img' and 'b' there (if they are non-nil).
//
// On success returns *oldest* buildRef entry (which may happen to be 'b' if
// it's the first metadata update ever or even nil if 'b' is nil).
func updateMetadata(ctx context.Context, obj *storage.Object, s storageImpl, img *imageRef, b *buildRef) (*buildRef, error) {
ts := storage.TimestampFromTime(clock.Now(ctx))
var imgRefJSON []byte
if img != nil {
var err error
if imgRefJSON, err = json.Marshal(img); err != nil {
return nil, errors.Annotate(err, "marshalling imageRef %v", img).Err()
}
}
var buildRefJSON []byte
if b != nil {
var err error
if buildRefJSON, err = json.Marshal(b); err != nil {
return nil, errors.Annotate(err, "marshalling buildRef %v", b).Err()
}
}
var oldest *buildRef
err := s.UpdateMetadata(ctx, obj, func(m *storage.Metadata) error {
oldest = nil // reset on retry
if imgRefJSON != nil {
m.Add(storage.Metadatum{
Key: imageRefMetaKey,
Timestamp: ts,
Value: string(imgRefJSON),
})
}
if buildRefJSON != nil {
m.Add(storage.Metadatum{
Key: buildRefMetaKey,
Timestamp: ts,
Value: string(buildRefJSON),
})
}
m.TrimUnimportant(50) // to avoid growing metadata size indefinitely
// Find the oldest non-broken entry.
buildRefs := m.Values(buildRefMetaKey) // most recent first
for i := len(buildRefs) - 1; i >= 0; i-- {
md := buildRefs[i]
var ref buildRef
if err := json.Unmarshal([]byte(md.Value), &ref); err != nil {
logging.Warningf(ctx, "Skipping bad buildRef metadata value %q", md.Value)
continue
}
ref.Timestamp = md.Timestamp.Time()
oldest = &ref
break
}
return nil
})
if err != nil {
return nil, errors.Annotate(err, "failed to update tarball metadata").Err()
}
return oldest, nil
}
// imageRefsFromMetadata deserializes imageRefs stored in the object metadata.
//
// Logs and skips invalid entries.
func imageRefsFromMetadata(ctx context.Context, obj *storage.Object) (out []imageRef) {
for _, md := range obj.Metadata.Values(imageRefMetaKey) {
var ref imageRef
if err := json.Unmarshal([]byte(md.Value), &ref); err != nil {
logging.Warningf(ctx, "Skipping bad imageRef metadata value %q", md.Value)
} else {
ref.Timestamp = md.Timestamp.Time()
out = append(out, ref)
}
}
return
}