blob: 45d2d9e0f355d9410fd08d7e021f6c5a18446354 [file] [log] [blame]
// Copyright 2018 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"context"
"crypto/sha1"
"encoding/hex"
"fmt"
"sort"
"strings"
"time"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/proto/google"
"go.chromium.org/luci/common/retry/transient"
"go.chromium.org/luci/grpc/grpcutil"
"go.chromium.org/luci/server/auth"
api "go.chromium.org/luci/cipd/api/cipd/v1"
"go.chromium.org/luci/cipd/common"
)
// Tag represents a "key:value" pair attached to some package instance.
//
// Tags exist as separate entities (as opposed to being a repeated property
// of Instance entity) to allow essentially unlimited number of them. Also we
// often do not want to fetch all tags when fetching Instance entity.
//
// ID is hex-encoded SHA1 of the tag. The parent entity is the corresponding
// Instance. The tag string can't be made a part of the key because the total
// key length (including entity kind names and all parent keys) is limited to
// 500 bytes and tags are allowed to be pretty long (up to 400 bytes).
//
// There's a possibility someone tries to abuse a collision in SHA1 hash
// used by TagID() to attach or detach some wrong tag. The chance is minuscule,
// and it's not a big deal if it happens now, but it may become important in the
// future once tags have their own ACLs. For the sake of paranoia, we double
// check 'Tag' field in all entities we touch.
//
// Compatible with the python version of the backend.
type Tag struct {
_kind string `gae:"$kind,InstanceTag"`
_extra datastore.PropertyMap `gae:"-,extra"`
ID string `gae:"$id"` // see TagID()
Instance *datastore.Key `gae:"$parent"` // key of corresponding Instance entity
Tag string `gae:"tag"` // the tag itself, as "k:v" pair
RegisteredBy string `gae:"registered_by"` // who added this tag
RegisteredTs time.Time `gae:"registered_ts"` // when it was added
}
// Proto returns cipd.Tag proto with information from this entity.
//
// Assumes the tag is valid.
func (t *Tag) Proto() *api.Tag {
kv := strings.SplitN(t.Tag, ":", 2)
if len(kv) != 2 {
panic(fmt.Sprintf("bad tag %q", t.Tag))
}
return &api.Tag{
Key: kv[0],
Value: kv[1],
AttachedBy: t.RegisteredBy,
AttachedTs: google.NewTimestamp(t.RegisteredTs),
}
}
// TagID calculates Tag entity ID (SHA1 digest) from the given tag.
//
// Panics if the tag is invalid.
func TagID(t *api.Tag) string {
tag := common.JoinInstanceTag(t)
if err := common.ValidateInstanceTag(tag); err != nil {
panic(err)
}
digest := sha1.Sum([]byte(tag))
return hex.EncodeToString(digest[:])
}
// AttachTags transactionally attaches a bunch of tags to an instance.
//
// Assumes inputs are already validated. Launches a transaction inside (and thus
// can't be a part of a transaction itself). Updates 'inst' in-place with the
// most recent instance state.
//
// Returns gRPC-tagged errors:
// NotFound if there's no such instance or package.
// FailedPrecondition if some processors are still running.
// Aborted if some processors have failed.
// Internal on tag ID collision.
func AttachTags(c context.Context, inst *Instance, tags []*api.Tag) error {
return Txn(c, "AttachTags", func(c context.Context) error {
if err := CheckInstanceReady(c, inst); err != nil {
return err
}
_, missing, err := checkExistingTags(c, inst, tags)
if err != nil {
return err
}
events := Events{}
now := clock.Now(c).UTC()
who := string(auth.CurrentIdentity(c))
for _, t := range missing {
t.RegisteredBy = who
t.RegisteredTs = now
events.Emit(&api.Event{
Kind: api.EventKind_INSTANCE_TAG_ATTACHED,
Package: inst.Package.StringID(),
Instance: inst.InstanceID,
Tag: t.Tag,
Who: who,
When: google.NewTimestamp(now),
})
}
if err := datastore.Put(c, missing); err != nil {
return transient.Tag.Apply(err)
}
return events.Flush(c)
})
}
// DetachTags detaches a bunch of tags from an instance.
//
// Assumes inputs are already validated. Launches a transaction inside (and thus
// can't be a part of a transaction itself).
//
// 'inst' is used only for its key.
func DetachTags(c context.Context, inst *Instance, tags []*api.Tag) error {
return Txn(c, "DetachTags", func(c context.Context) error {
existing, _, err := checkExistingTags(c, inst, tags)
if err != nil {
return err
}
if err := datastore.Delete(c, existing); err != nil {
return transient.Tag.Apply(err)
}
events := Events{}
for _, t := range existing {
events.Emit(&api.Event{
Kind: api.EventKind_INSTANCE_TAG_DETACHED,
Package: inst.Package.StringID(),
Instance: inst.InstanceID,
Tag: t.Tag,
})
}
return events.Flush(c)
})
}
// ResolveTag searches for a given tag among all instances of the package and
// returns an ID of the instance the tag is attached to.
//
// Assumes inputs are already validated. Doesn't double check the instance
// exists.
//
// Returns gRPC-tagged errors:
// NotFound if there's no such tag at all.
// FailedPrecondition if the tag resolves to multiple instances.
func ResolveTag(c context.Context, pkg string, tag *api.Tag) (string, error) {
// TODO(vadimsh): Cache the result of the resolution. This is generally not
// trivial to do right without race conditions, preserving the consistency
// guarantees of the API. This will become simpler once we have a notion of
// unique tags.
q := datastore.NewQuery("InstanceTag").
Ancestor(PackageKey(c, pkg)).
Eq("tag", common.JoinInstanceTag(tag)).
KeysOnly(true).
Limit(2)
var tags []*Tag
switch err := datastore.GetAll(c, q, &tags); {
case err != nil:
return "", errors.Annotate(err, "failed to query tags").Tag(transient.Tag).Err()
case len(tags) == 0:
return "", errors.Reason("no such tag").Tag(grpcutil.NotFoundTag).Err()
case len(tags) > 1:
return "", errors.Reason("ambiguity when resolving the tag, more than one instance has it").Tag(grpcutil.FailedPreconditionTag).Err()
default:
return tags[0].Instance.StringID(), nil
}
}
// ListInstanceTags returns all tags attached to an instance, sorting them by
// the tag key first, and then by the timestamp (most recent first).
//
// Returns an empty list if there's no such instance at all.
func ListInstanceTags(c context.Context, inst *Instance) (out []*Tag, err error) {
// TODO(vadimsh): Sorting by tag key requires adding the key as a field in the
// entity and setting up a composite index (key, -registered_ts). This change
// requires a migration of existing entities. We punt on this for now and sort
// in memory instead (just like we did on the client before). The most heavily
// tagged instances in our datastore have few hundred tags at most, so this is
// bearable.
q := datastore.NewQuery("InstanceTag").Ancestor(datastore.KeyForObj(c, inst))
if datastore.GetAll(c, q, &out); err != nil {
return nil, errors.Annotate(err, "datastore query failed").Tag(transient.Tag).Err()
}
sort.Slice(out, func(i, j int) bool {
if k1, k2 := tagKey(out[i].Tag), tagKey(out[j].Tag); k1 != k2 {
return k1 < k2
}
if !out[i].RegisteredTs.Equal(out[j].RegisteredTs) {
return out[i].RegisteredTs.After(out[j].RegisteredTs)
}
return out[i].Tag < out[j].Tag
})
return
}
// tagKey takes "key:<stuff>" and returns "key".
//
// It is a faster version of common.ParseInstanceTags that skips validation
// (since stored entities are already validated).
//
// We micro-optimized this part because comparator in ListInstanceTags is a hot
// spot when sorting a lot of entities.
func tagKey(kv string) string {
if idx := strings.IndexRune(kv, ':'); idx != -1 {
return kv[:idx]
}
return kv
}
// checkExistingTags takes a list of tags and returns the ones that exist
// in the datastore and ones that don't.
//
// Checks 'Tag' field on existing tags, thus safeguarding against malicious
// SHA1 collisions in TagID().
//
// Tags in 'miss' list have only their key and 'Tag' fields set and nothing
// more.
func checkExistingTags(c context.Context, inst *Instance, tags []*api.Tag) (exist, miss []*Tag, err error) {
instKey := datastore.KeyForObj(c, inst)
tagEnts := make([]*Tag, len(tags))
for i, tag := range tags {
tagEnts[i] = &Tag{
ID: TagID(tag),
Instance: instKey,
}
}
return fetchTags(c, tagEnts, func(i int) *api.Tag { return tags[i] })
}
// fetchTags fetches given tag entities and categorized them into existing and
// missing ones.
//
// The entities doesn't have to be in same entity group.
//
// Checks 'Tag' field on existing tags (by comparing it to what 'expectedTag'
// callback returns for the corresponding index), thus safeguarding against
// malicious SHA1 collisions in TagID().
func fetchTags(c context.Context, tagEnts []*Tag, expectedTag func(idx int) *api.Tag) (exist, miss []*Tag, err error) {
// Try to grab all entities and bail on unexpected errors.
existCount := 0
missCount := 0
if err := datastore.Get(c, tagEnts); err != nil {
merr, ok := err.(errors.MultiError)
if !ok {
return nil, nil, errors.Annotate(err, "failed to fetch tags").Tag(transient.Tag).Err()
}
for i, err := range merr {
switch err {
case nil:
existCount++
case datastore.ErrNoSuchEntity:
missCount++
default:
return nil, nil, errors.Annotate(err, "failed to fetch tag with ID %q", tagEnts[i].ID).Tag(transient.Tag).Err()
}
}
} else {
existCount = len(tagEnts)
}
if existCount != 0 {
exist = make([]*Tag, 0, existCount)
}
if missCount != 0 {
miss = make([]*Tag, 0, missCount)
}
for i, ent := range tagEnts {
switch kv := common.JoinInstanceTag(expectedTag(i)); {
case ent.Tag == kv:
exist = append(exist, ent)
case ent.Tag == "":
ent.Tag = kv // so the caller knows what tag this is
miss = append(miss, ent)
default: // ent.Tag != kv
return nil, nil, errors.Reason("tag %q collides with tag %q, refusing to touch it", kv, ent.Tag).
Tag(grpcutil.InternalTag).Err()
}
}
return
}