~jameinel/juju-core/api-registry-tracks-type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// Copyright 2012, 2013 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package store

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"launchpad.net/juju-core/charm"
)

// PublishBazaarBranch checks out the Bazaar branch from burl and
// publishes its latest revision at urls in the given store.
// The digest parameter must be the most recent known Bazaar
// revision id for the branch tip. If publishing this specific digest
// for these URLs has been attempted already, the publishing
// procedure may abort early. The published digest is the Bazaar
// revision id of the checked out branch's tip, though, which may
// differ from the digest parameter.
func PublishBazaarBranch(store *Store, urls []*charm.URL, burl string, digest string) error {

	// Prevent other publishers from updating these specific URLs
	// concurrently.
	lock, err := store.LockUpdates(urls)
	if err != nil {
		return err
	}
	defer lock.Unlock()

	var branchDir string
NewTip:
	// Prepare the charm publisher. This will compute the revision
	// to be assigned to the charm, and it will also fail if the
	// operation is unnecessary because charms are up-to-date.
	pub, err := store.CharmPublisher(urls, digest)
	if err != nil {
		return err
	}

	// Figure if publishing this charm was already attempted before and
	// failed. We won't try again endlessly if so. In the future we may
	// retry automatically in certain circumstances.
	event, err := store.CharmEvent(urls[0], digest)
	if err == nil && event.Kind != EventPublished {
		return fmt.Errorf("charm publishing previously failed: %s", strings.Join(event.Errors, "; "))
	} else if err != nil && err != ErrNotFound {
		return err
	}

	if branchDir == "" {
		// Retrieve the branch with a lightweight checkout, so that it
		// builds a working tree as cheaply as possible. History
		// doesn't matter here.
		tempDir, err := ioutil.TempDir("", "publish-branch-")
		if err != nil {
			return err
		}
		defer os.RemoveAll(tempDir)
		branchDir = filepath.Join(tempDir, "branch")
		output, err := exec.Command("bzr", "checkout", "--lightweight", burl, branchDir).CombinedOutput()
		if err != nil {
			return outputErr(output, err)
		}

		// Pick actual digest from tip. Publishing the real tip
		// revision rather than the revision for the digest provided is
		// strictly necessary to prevent a race condition. If the
		// provided digest was published instead, there's a chance
		// another publisher concurrently running could have found a
		// newer revision and published that first, and the digest
		// parameter provided is in fact an old version that would
		// overwrite the new version.
		tipDigest, err := bzrRevisionId(branchDir)
		if err != nil {
			return err
		}
		if tipDigest != digest {
			digest = tipDigest
			goto NewTip
		}
	}

	ch, err := charm.ReadDir(branchDir)
	if err == nil {
		// Hand over the charm to the store for bundling and
		// streaming its content into the database.
		err = pub.Publish(ch)
		if err == ErrUpdateConflict {
			// A conflict may happen in edge cases if the whole
			// locking mechanism fails due to an expiration event,
			// and then the expired concurrent publisher revives
			// for whatever reason and attempts to finish
			// publishing. The state of the system is still
			// consistent in that case, and the error isn't logged
			// since the revision was properly published before.
			return err
		}
	}

	// Publishing is done. Log failure or error.
	event = &CharmEvent{
		URLs:   urls,
		Digest: digest,
	}
	if err == nil {
		event.Kind = EventPublished
		event.Revision = pub.Revision()
	} else {
		event.Kind = EventPublishError
		event.Errors = []string{err.Error()}
	}
	if logerr := store.LogCharmEvent(event); logerr != nil {
		if err == nil {
			err = logerr
		} else {
			err = fmt.Errorf("%v; %v", err, logerr)
		}
	}
	return err
}

// bzrRevisionId returns the Bazaar revision id for the branch in branchDir.
func bzrRevisionId(branchDir string) (string, error) {
	cmd := exec.Command("bzr", "revision-info")
	cmd.Dir = branchDir
	stderr := &bytes.Buffer{}
	cmd.Stderr = stderr
	output, err := cmd.Output()
	if err != nil {
		output = append(output, '\n')
		output = append(output, stderr.Bytes()...)
		return "", outputErr(output, err)
	}
	pair := bytes.Fields(output)
	if len(pair) != 2 {
		output = append(output, '\n')
		output = append(output, stderr.Bytes()...)
		return "", fmt.Errorf(`invalid output from "bzr revision-info": %s`, output)
	}
	return string(pair[1]), nil
}

// outputErr returns an error that assembles some command's output and its
// error, if both output and err are set, and returns only err if output is nil.
func outputErr(output []byte, err error) error {
	if len(output) > 0 {
		return fmt.Errorf("%v\n%s", err, output)
	}
	return err
}