Repository: google/ko
Pull request #109 (2019-11-07 21:41:44)

Include tag in resolved references
If a single tag is explicitly set (i.e. it's not "latest"), include that
in the reference that gets rendered in the yaml.

This is really useful for tracking releases.

Fixes https://github.com/google/ko/issues/108

Also fixes a TODO.


Patch:
diff --git a/pkg/publish/default.go b/pkg/publish/default.go
index fcea4217..3eedb228 100644
--- a/pkg/publish/default.go
+++ b/pkg/publish/default.go
@@ -97,22 +97,28 @@ func (d *defalt) Publish(img v1.Image, s string) (name.Reference, error) {
 	// https://github.com/google/go-containerregistry/issues/212
 	s = strings.ToLower(s)
 
-	for _, tagName := range d.tags {
+	ro := []remote.Option{remote.WithAuth(d.auth), remote.WithTransport(d.t)}
+	no := []name.Option{}
+	if d.insecure {
+		no = append(no, name.Insecure)
+	}
 
-		var os []name.Option
-		if d.insecure {
-			os = []name.Option{name.Insecure}
-		}
-		tag, err := name.NewTag(fmt.Sprintf("%s/%s:%s", d.base, d.namer(s), tagName), os...)
+	for i, tagName := range d.tags {
+		tag, err := name.NewTag(fmt.Sprintf("%s/%s:%s", d.base, d.namer(s), tagName), no...)
 		if err != nil {
 			return nil, err
 		}
 
-		log.Printf("Publishing %v", tag)
-		// TODO: This is slow because we have to load the image multiple times.
-		// Figure out some way to publish the manifest with another tag.
-		if err := remote.Write(tag, img, remote.WithAuth(d.auth), remote.WithTransport(d.t)); err != nil {
-			return nil, err
+		if i == 0 {
+			log.Printf("Publishing %v", tag)
+			if err := remote.Write(tag, img, ro...); err != nil {
+				return nil, err
+			}
+		} else {
+			log.Printf("Tagging %v", tag)
+			if err := remote.Tag(tag, img, ro...); err != nil {
+				return nil, err
+			}
 		}
 	}
 
@@ -120,7 +126,13 @@ func (d *defalt) Publish(img v1.Image, s string) (name.Reference, error) {
 	if err != nil {
 		return nil, err
 	}
-	dig, err := name.NewDigest(fmt.Sprintf("%s/%s@%s", d.base, d.namer(s), h))
+	ref := fmt.Sprintf("%s/%s@%s", d.base, d.namer(s), h)
+	if len(d.tags) == 1 && d.tags[0] != defaultTags[0] {
+		// If a single tag is explicitly set (not latest), then this
+		// is probably a release, so include the tag in the reference.
+		ref = fmt.Sprintf("%s/%s:%s@%s", d.base, d.namer(s), d.tags[0], h)
+	}
+	dig, err := name.NewDigest(ref)
 	if err != nil {
 		return nil, err
 	}


Test patch:
diff --git a/pkg/publish/default_test.go b/pkg/publish/default_test.go
index 9f55803b..aa98c598 100644
--- a/pkg/publish/default_test.go
+++ b/pkg/publish/default_test.go
@@ -221,3 +221,74 @@ func TestDefaultWithTags(t *testing.T) {
 		t.Errorf("Tag v1.2.3 was not created.")
 	}
 }
+
+func TestDefaultWithReleaseTag(t *testing.T) {
+	img, err := random.Image(1024, 1)
+	if err != nil {
+		t.Fatalf("random.Image() = %v", err)
+	}
+	base := "blah"
+	releaseTag := "v1.2.3"
+	importpath := "github.com/Google/go-containerregistry/cmd/crane"
+	expectedRepo := fmt.Sprintf("%s/%s", base, strings.ToLower(importpath))
+	headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo)
+	initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo)
+	manifestPath := fmt.Sprintf("/v2/%s/manifests/", expectedRepo)
+
+	createdTags := make(map[string]struct{})
+
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath {
+			http.Error(w, "NotFound", http.StatusNotFound)
+			return
+		}
+		switch {
+		case r.URL.Path == "/v2/":
+			w.WriteHeader(http.StatusOK)
+		case r.URL.Path == initiatePath:
+			if r.Method != http.MethodPost {
+				t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost)
+			}
+			http.Error(w, "Mounted", http.StatusCreated)
+		case strings.HasPrefix(r.URL.Path, manifestPath):
+			if r.Method != http.MethodPut {
+				t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut)
+			}
+
+			createdTags[strings.TrimPrefix(r.URL.Path, manifestPath)] = struct{}{}
+
+			http.Error(w, "Created", http.StatusCreated)
+		default:
+			t.Fatalf("Unexpected path: %v", r.URL.Path)
+		}
+	}))
+	defer server.Close()
+	u, err := url.Parse(server.URL)
+	if err != nil {
+		t.Fatalf("url.Parse(%v) = %v", server.URL, err)
+	}
+	tag, err := name.NewTag(fmt.Sprintf("%s/%s:notLatest", u.Host, expectedRepo))
+	if err != nil {
+		t.Fatalf("NewTag() = %v", err)
+	}
+
+	repoName := fmt.Sprintf("%s/%s", u.Host, base)
+
+	def, err := NewDefault(repoName, WithTags([]string{releaseTag}))
+	if err != nil {
+		t.Errorf("NewDefault() = %v", err)
+	}
+	if d, err := def.Publish(img, importpath); err != nil {
+		t.Errorf("Publish() = %v", err)
+	} else if !strings.HasPrefix(d.String(), repoName) {
+		t.Errorf("Publish() = %v, wanted prefix %v", d, tag.Repository)
+	} else if !strings.HasSuffix(d.Context().String(), strings.ToLower(importpath)) {
+		t.Errorf("Publish() = %v, wanted suffix %v", d.Context(), md5Hash(importpath))
+	} else if !strings.Contains(d.String(), releaseTag) {
+		t.Errorf("Publish() = %v, wanted tag included: %v", d.String(), releaseTag)
+	}
+
+	if _, ok := createdTags["v1.2.3"]; !ok {
+		t.Errorf("Tag v1.2.3 was not created.")
+	}
+}