Skip to content

Commit

Permalink
Include tags when --push=false is set
Browse files Browse the repository at this point in the history
- Refactor noop into its own publisher.
- Use :tag@sha format in digest when --push=false is set.
- Allow --tag-only to be used with --push=false. This includes
  validations that prevent --tag-only to be used with no tags or the
  `latest` tag.

Signed-off-by: Adam Kaplan <[email protected]>
  • Loading branch information
adambkaplan committed Aug 19, 2022
1 parent 84356b8 commit f508b06
Show file tree
Hide file tree
Showing 5 changed files with 317 additions and 22 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ jobs:
docker run ${testimg} --wait=false -f HEAD
fi
# Check that building without push prints the tag (and sha)
go run ./ build --push=false -t test ./test | grep ":test@sha256:"
go run ./ build --push=false -t test --tag-only ./test | grep ":test$"
# Check that using ldflags to set variables works.
cat > .ko.yaml << EOF
builds:
Expand Down
31 changes: 9 additions & 22 deletions pkg/commands/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,15 @@ func makePublisher(po *options.PublishOptions) (publish.Interface, error) {
// If not publishing, at least generate a digest to simulate
// publishing.
if len(publishers) == 0 {
publishers = append(publishers, nopPublisher{
repoName: repoName,
namer: namer,
})
noop, err := publish.NewNoOp(repoName,
publish.NoOpWithNamer(namer),
publish.NoOpWithTags(po.Tags),
publish.NoOpWithTagOnly(po.TagOnly),
)
if err != nil {
return nil, err
}
publishers = append(publishers, noop)
}

return publish.MultiPublisher(publishers...), nil
Expand All @@ -251,24 +256,6 @@ func makePublisher(po *options.PublishOptions) (publish.Interface, error) {
return publish.NewCaching(innerPublisher)
}

// nopPublisher simulates publishing without actually publishing anything, to
// provide fallback behavior when the user configures no push destinations.
type nopPublisher struct {
repoName string
namer publish.Namer
}

func (n nopPublisher) Publish(_ context.Context, br build.Result, s string) (name.Reference, error) {
s = strings.TrimPrefix(s, build.StrictScheme)
h, err := br.Digest()
if err != nil {
return nil, err
}
return name.NewDigest(fmt.Sprintf("%s@%s", n.namer(n.repoName, s), h))
}

func (n nopPublisher) Close() error { return nil }

// resolvedFuture represents a "future" for the bytes of a resolved file.
type resolvedFuture chan []byte

Expand Down
99 changes: 99 additions & 0 deletions pkg/publish/noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2022 Google LLC All Rights Reserved.
//
// 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 publish

import (
"context"
"errors"
"fmt"
"strings"

"github.com/google/go-containerregistry/pkg/name"
"github.com/google/ko/pkg/build"
)

// Original spelling was preserved when this was refactored out of pkg/commands
type nopPublisher struct {
repoName string
namer Namer
tag string
tagOnly bool
}

type noOpOpener struct {
repoName string
namer Namer
tags []string
tagOnly bool
}

// NoOpOption provides functional options to the NoOp publisher.
type NoOpOption func(*noOpOpener)

func (o *noOpOpener) Open() (Interface, error) {
tag := defaultTags[0]
if o.tagOnly {
// Replicate the tag-only validations in the default publisher
if len(o.tags) != 1 {
return nil, errors.New("must specify exactly one tag to resolve images into tag-only references")
}
if o.tags[0] == defaultTags[0] {
return nil, errors.New("latest tag cannot be used in tag-only references")
}
}
// If one or more tags are specified, use the first tag in the list
if len(o.tags) >= 1 {
tag = o.tags[0]
}
return &nopPublisher{
repoName: o.repoName,
namer: o.namer,
tag: tag,
tagOnly: o.tagOnly,
}, nil
}

// NewNoOp returns a publisher.Interface that simulates publishing without actually publishing
// anything, to provide fallback behavior when the user configures no push destinations.
func NewNoOp(baseName string, options ...NoOpOption) (Interface, error) {
nop := &noOpOpener{
repoName: baseName,
namer: identity,
}
for _, option := range options {
option(nop)
}
return nop.Open()
}

// Publish implements publish.Interface
func (n *nopPublisher) Publish(_ context.Context, br build.Result, s string) (name.Reference, error) {
s = strings.TrimPrefix(s, build.StrictScheme)
h, err := br.Digest()
if err != nil {
return nil, err
}
// If the tag is not empty or is not "latest", use the :tag@sha suffix
if n.tag != "" || n.tag != defaultTags[0] {
// If tag only, just return the tag
if n.tagOnly {
return name.NewTag(fmt.Sprintf("%s:%s", n.namer(n.repoName, s), n.tag))
}
return name.NewDigest(fmt.Sprintf("%s:%s@%s", n.namer(n.repoName, s), n.tag, h))
}
return name.NewDigest(fmt.Sprintf("%s@%s", n.namer(n.repoName, s), h))
}

func (n *nopPublisher) Close() error { return nil }
187 changes: 187 additions & 0 deletions pkg/publish/noop_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright 2022 Google LLC All Rights Reserved.
//
// 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 publish_test

import (
"context"
"fmt"
"strings"
"testing"

"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/google/ko/pkg/build"
"github.com/google/ko/pkg/publish"
)

func TestNoOp(t *testing.T) {
repoName := "quarkus.io/charm"
importPath := "crane"
noop, err := publish.NewNoOp(repoName)
if err != nil {
t.Fatalf("NewNoOp() = %v", err)
}
img, err := random.Image(1024, 1)
if err != nil {
t.Fatalf("random.Image() = %v", err)
}
ref, err := noop.Publish(context.TODO(), img, build.StrictScheme+importPath)
if err != nil {
t.Fatalf("Publish() = %v", err)
}
if !strings.HasPrefix(ref.String(), repoName) {
t.Errorf("Publish() = %v, wanted preifx %s", ref, repoName)
}
}

func TestNoOpWithCustomNamer(t *testing.T) {
repoName := "quarkus.io/charm"
importPath := "crane"
noop, err := publish.NewNoOp(repoName, publish.NoOpWithNamer(md5Hash))
if err != nil {
t.Fatalf("NewNoOp() = %v", err)
}
img, err := random.Image(1024, 1)
if err != nil {
t.Fatalf("random.Image() = %v", err)
}
ref, err := noop.Publish(context.TODO(), img, build.StrictScheme+importPath)
if err != nil {
t.Fatalf("Publish() = %v", err)
}
if !strings.HasPrefix(ref.String(), repoName) {
t.Errorf("Publish() = %v, wanted preifx %s", ref, repoName)
}
if !strings.HasSuffix(ref.Context().String(), md5Hash("", strings.ToLower(importPath))) {
t.Errorf("Publish() = %v, wanted suffix %v", ref.Context(), md5Hash("", importPath))
}
}

func TestNoOpWithTags(t *testing.T) {
cases := []struct {
name string
tags []string
expectedTag string
expectOpenError bool
}{
{
name: "no tags",
},
{
name: "latest tag",
tags: []string{"latest"},
},
{
name: "multiple tags",
tags: []string{"v0.1", "v0.1.1"},
expectedTag: "v0.1",
},
{
name: "single tag",
tags: []string{"v0.1"},
expectedTag: "v0.1",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
repoName := "quarkus.io/charm"
importPath := "crane"
noop, err := publish.NewNoOp(repoName, publish.NoOpWithTags(tc.tags))
if tc.expectOpenError {
if err == nil {
t.Error("NewNoOp() - expected error, got none")
}
return
}
if err != nil {
t.Fatalf("NewNoOp() = %v", err)
}
img, err := random.Image(1024, 1)
if err != nil {
t.Fatalf("random.Image() = %v", err)
}
ref, err := noop.Publish(context.TODO(), img, build.StrictScheme+importPath)
if err != nil {
t.Fatalf("Publish() = %v", err)
}
if !strings.HasPrefix(ref.String(), repoName) {
t.Errorf("Publish() = %v, wanted preifx %s", ref, repoName)
}
if tc.expectedTag != "" && !strings.Contains(ref.String(), fmt.Sprintf(":%s@", tc.expectedTag)) {
t.Errorf("Publish() = %v, expected tag %s", ref.String(), tc.expectedTag)
}
})
}
}

func TestNoOpWithTagOnly(t *testing.T) {
cases := []struct {
name string
tags []string
expectedTag string
expectOpenError bool
}{
{
name: "no tags",
expectOpenError: true,
},
{
name: "latest tag",
tags: []string{"latest"},
expectOpenError: true,
},
{
name: "multiple tags",
tags: []string{"v0.1", "v0.1.1"},
expectOpenError: true,
},
{
name: "single tag",
tags: []string{"v0.1"},
expectedTag: "v0.1",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
repoName := "quarkus.io/charm"
importPath := "crane"
noop, err := publish.NewNoOp(repoName,
publish.NoOpWithTags(tc.tags),
publish.NoOpWithTagOnly(true))
if tc.expectOpenError {
if err == nil {
t.Error("NewNoOp() - expected error, got none")
}
return
}
if err != nil {
t.Fatalf("NewNoOp() = %v", err)
}
img, err := random.Image(1024, 1)
if err != nil {
t.Fatalf("random.Image() = %v", err)
}
ref, err := noop.Publish(context.TODO(), img, build.StrictScheme+importPath)
if err != nil {
t.Fatalf("Publish() = %v", err)
}
if !strings.HasPrefix(ref.String(), repoName) {
t.Errorf("Publish() = %v, wanted preifx %s", ref, repoName)
}
if tc.expectedTag != "" && !strings.HasSuffix(ref.String(), fmt.Sprintf(":%s", tc.expectedTag)) {
t.Errorf("Publish() = %v, expected only tag %s", ref.String(), tc.expectedTag)
}
})
}
}
18 changes: 18 additions & 0 deletions pkg/publish/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,21 @@ func Insecure(b bool) Option {
return nil
}
}

func NoOpWithNamer(n Namer) NoOpOption {
return func(noo *noOpOpener) {
noo.namer = n
}
}

func NoOpWithTags(t []string) NoOpOption {
return func(noo *noOpOpener) {
noo.tags = t
}
}

func NoOpWithTagOnly(to bool) NoOpOption {
return func(noo *noOpOpener) {
noo.tagOnly = to
}
}

0 comments on commit f508b06

Please sign in to comment.