blob: 0812212c90a8cc9a7886ec2d0cc18d08fab0be67 [file] [log] [blame]
// Copyright 2015 The Vanadium 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 (
"fmt"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/naming"
"v.io/v23/security"
"v.io/v23/security/access"
"v.io/v23/services/application"
"v.io/v23/services/permissions"
"v.io/v23/verror"
"v.io/x/lib/cmdline"
"v.io/x/ref/lib/v23cmd"
"v.io/x/ref/services/internal/binarylib"
"v.io/x/ref/services/repository"
)
// TODO(caprita): Add unit test.
// TODO(caprita): Extend to include env, args, packages.
var cmdPublish = &cmdline.Command{
Runner: v23cmd.RunnerFunc(runPublish),
Name: "publish",
Short: "Publish the given application(s).",
Long: `
Publishes the given application(s) to the binary and application servers.
The binaries should be in $JIRI_ROOT/release/go/bin/[<GOOS>_<GOARCH>] by default (can be overrriden with --from).
By default the binary name is used as the name of the application envelope, and as the
title in the envelope. However, <envelope-name> and <title> can be specified explicitly
using :<envelope-name> and @<title>.
The binary is published as <binserv>/<binary name>/<GOOS>-<GOARCH>/<TIMESTAMP>.
The application envelope is published as <appserv>/<envelope-name>/<TIMESTAMP>.
Optionally, adds blessing patterns to the Read and Resolve AccessLists.`,
ArgsName: "<binary name>[:<envelope-name>][@<title>] ...",
}
var binaryService, applicationService, readBlessings, goarchFlag, goosFlag, fromFlag string
var addPublisher bool
var minValidPublisherDuration time.Duration
func init() {
cmdPublish.Flags.StringVar(&binaryService, "binserv", "binaries", "Name of binary service.")
cmdPublish.Flags.StringVar(&applicationService, "appserv", "applications", "Name of application service.")
cmdPublish.Flags.StringVar(&goosFlag, "goos", runtime.GOOS, "GOOS for application. The default is the value of runtime.GOOS.")
cmdPublish.Flags.Lookup("goos").DefValue = "<runtime.GOOS>"
cmdPublish.Flags.StringVar(&goarchFlag, "goarch", runtime.GOARCH, "GOARCH for application. The default is the value of runtime.GOARCH.")
cmdPublish.Flags.Lookup("goarch").DefValue = "<runtime.GOARCH>"
cmdPublish.Flags.StringVar(&readBlessings, "readers", "dev.v.io", "If non-empty, comma-separated blessing patterns to add to Read and Resolve AccessList.")
cmdPublish.Flags.BoolVar(&addPublisher, "add-publisher", true, "If true, add a publisher blessing to the application envelope")
cmdPublish.Flags.DurationVar(&minValidPublisherDuration, "publisher-min-validity", 30*time.Hour, "Publisher blessings that are valid for less than this amount of time are considered invalid")
cmdPublish.Flags.StringVar(&fromFlag, "from", "", "Location of binaries to be published. Defaults to $JIRI_ROOT/release/go/bin/[<GOOS>_<GOARCH>]")
}
func setAccessLists(ctx *context.T, env *cmdline.Env, von string) error {
if readBlessings == "" {
return nil
}
perms, version, err := permissions.ObjectClient(von).GetPermissions(ctx)
if err != nil {
// TODO(caprita): This is a workaround until we sort out the
// default AccessLists for applicationd (see issue #1317). At that
// time, uncomment the line below.
//
// return err
perms = make(access.Permissions)
}
for _, blessing := range strings.Split(readBlessings, ",") {
for _, tag := range []access.Tag{access.Read, access.Resolve} {
perms.Add(security.BlessingPattern(blessing), string(tag))
}
}
if err := permissions.ObjectClient(von).SetPermissions(ctx, perms, version); err != nil {
return err
}
fmt.Fprintf(env.Stdout, "Added patterns %q to Read,Resolve AccessList for %q\n", readBlessings, von)
return nil
}
func publishOne(ctx *context.T, env *cmdline.Env, binPath, binary string) error {
binaryName, envelopeName, title := binary, binary, binary
binaryRE := regexp.MustCompile(`^([^:@]+)(:[^@]+)?(@.+)?$`)
if parts := binaryRE.FindStringSubmatch(binary); len(parts) == 4 {
binaryName = parts[1]
envelopeName, title = binaryName, binaryName
if len(parts[2]) > 1 {
envelopeName = parts[2][1:]
}
if len(parts[3]) > 1 {
title = parts[3][1:]
}
} else {
return fmt.Errorf("invalid binary spec (%v)", binary)
}
// Step 1, upload the binary to the binary service.
// TODO(caprita): Instead of the current timestamp, use each binary's
// BuildTimestamp from the buildinfo.
timestamp := time.Now().UTC().Format(time.RFC3339)
binaryVON := naming.Join(binaryService, binaryName, fmt.Sprintf("%s-%s", goosFlag, goarchFlag), timestamp)
binaryFile := filepath.Join(binPath, binaryName)
var binarySig *security.Signature
var err error
for i := 0; ; i++ {
binarySig, err = binarylib.UploadFromFile(ctx, binaryVON, binaryFile)
if verror.ErrorID(err) == verror.ErrExist.ID {
newTS := fmt.Sprintf("%s-%d", timestamp, i+1)
binaryVON = naming.Join(binaryService, binaryName, fmt.Sprintf("%s-%s", goosFlag, goarchFlag), newTS)
continue
}
if err == nil {
break
}
return err
}
fmt.Fprintf(env.Stdout, "Binary %q uploaded from file %s\n", binaryVON, binaryFile)
// Step 2, set the perms for the uploaded binary.
if err := setAccessLists(ctx, env, binaryVON); err != nil {
return err
}
// Step 3, download existing envelope (or create a new one), update, and
// upload to application service.
// TODO(caprita): use the profile detection machinery and/or let user
// specify the profile by hand.
profile := fmt.Sprintf("%s-%s", goosFlag, goarchFlag)
appVON := naming.Join(applicationService, envelopeName)
appClient := repository.ApplicationClient(appVON)
envelope, err := appClient.Match(ctx, []string{profile})
// TODO(caprita): Fix https://github.com/vanadium/issues/issues/679
if errID := verror.ErrorID(err); errID == verror.ErrNoExist.ID || errID == "v.io/x/ref/services/application/applicationd.InvalidSuffix" {
// There was nothing published yet, create a new envelope.
envelope = application.Envelope{Title: title}
} else if err != nil {
return err
} else {
// We are going to be updating an existing envelope
// Complain if a title was specified explicitly and does not match the one in the
// envelope, because we are not going to update the one in the envelope
if title != binaryName && title != envelope.Title {
return fmt.Errorf("Specified title (%v) does not match title in existing envelope (%v)", title, envelope.Title)
}
}
envelope.Binary.File = binaryVON
if addPublisher {
publisher, err := getPublisherBlessing(ctx, strings.Join([]string{"apps", "published", title}, security.ChainSeparator))
if err != nil {
return err
}
envelope.Publisher = publisher
envelope.Binary.Signature = *binarySig
} else {
// We must explicitly clear these fields because we might be trying to update
// an envelope that previously pointed at a signed binary.
envelope.Binary.Signature = security.Signature{}
envelope.Publisher = security.Blessings{}
}
appVON = naming.Join(appVON, timestamp)
appClient = repository.ApplicationClient(appVON)
if err := appClient.Put(ctx, profile, envelope, false); err != nil {
// NOTE(caprita): We don't retry if an envelope already exists
// at the versioned name, as we do when uploading binaries. In
// the case of binaries, it's likely that the same binary is
// uploaded more than once in a given second, due to apps
// sharing the same binary. The scenarios where the same app is
// published repeatedly in a short time-frame are expected to be
// rare, and the operator can retry manually in such cases.
return err
}
fmt.Fprintf(env.Stdout, "Published %q\n", appVON)
// Step 4, set the perms for the uploaded envelope.
if err := setAccessLists(ctx, env, appVON); err != nil {
return err
}
return nil
}
func runPublish(ctx *context.T, env *cmdline.Env, args []string) error {
if expectedMin, got := 1, len(args); got < expectedMin {
return env.UsageErrorf("publish: incorrect number of arguments, expected at least %d, got %d", expectedMin, got)
}
binaries := args
binPath := fromFlag
if binPath == "" {
vroot := env.Vars["JIRI_ROOT"]
if vroot == "" {
return env.UsageErrorf("publish: $JIRI_ROOT environment variable should be set")
}
binPath = filepath.Join(vroot, "release/go/bin")
if goosFlag != runtime.GOOS || goarchFlag != runtime.GOARCH {
binPath = filepath.Join(binPath, fmt.Sprintf("%s_%s", goosFlag, goarchFlag))
}
}
if fi, err := os.Stat(binPath); err != nil {
return env.UsageErrorf("publish: failed to stat %v: %v", binPath, err)
} else if !fi.IsDir() {
return env.UsageErrorf("publish: %v is not a directory", binPath)
}
if binaryService == "" {
return env.UsageErrorf("publish: --binserv must point to a binary service name")
}
if applicationService == "" {
return env.UsageErrorf("publish: --appserv must point to an application service name")
}
var lastErr error
for _, b := range binaries {
if err := publishOne(ctx, env, binPath, b); err != nil {
fmt.Fprintf(env.Stderr, "Failed to publish %q: %v\n", b, err)
lastErr = err
}
}
return lastErr
}
func getPublisherBlessing(ctx *context.T, extension string) (security.Blessings, error) {
p := v23.GetPrincipal(ctx)
bDef, _ := p.BlessingStore().Default()
b, err := p.Bless(p.PublicKey(), bDef, extension, security.UnconstrainedUse())
if err != nil {
return security.Blessings{}, err
}
// We need to make sure that the blessing is usable as a publisher blessing -- in
// practice this current means that it has no caveats other than expiration. We
// test this by putting it into a call object and verifying that the blessing will
// not be rejected
call := security.NewCall(&security.CallParams{
RemoteBlessings: b,
LocalBlessings: bDef,
LocalPrincipal: p,
Timestamp: time.Now().Add(minValidPublisherDuration),
})
accepted, rejected := security.RemoteBlessingNames(ctx, call)
if len(accepted) == 0 {
return security.Blessings{}, fmt.Errorf("All blessings are invalid: %v", rejected)
}
if len(rejected) > 0 {
fmt.Fprintf(os.Stderr, "Warning: Some invalid blessings are present: %v", rejected)
}
return b, nil
}