Skip to content

Commit

Permalink
Add go module support (ko-build#60)
Browse files Browse the repository at this point in the history
* Add go module support

* Make localimports work for go modules
  • Loading branch information
jonjohnsonjr authored Jul 13, 2019
1 parent d356007 commit 3566d3f
Show file tree
Hide file tree
Showing 47 changed files with 10,885 additions and 74 deletions.
19 changes: 19 additions & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 56 additions & 7 deletions pkg/build/gobuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"errors"
gb "go/build"
"io"
Expand All @@ -26,6 +27,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/mutate"
Expand All @@ -46,6 +48,7 @@ type gobuild struct {
creationTime v1.Time
build builder
disableOptimizations bool
mod *modInfo
}

// Option is a functional option for NewGo.
Expand All @@ -56,6 +59,7 @@ type gobuildOpener struct {
creationTime v1.Time
build builder
disableOptimizations bool
mod *modInfo
}

func (gbo *gobuildOpener) Open() (Interface, error) {
Expand All @@ -67,15 +71,39 @@ func (gbo *gobuildOpener) Open() (Interface, error) {
creationTime: gbo.creationTime,
build: gbo.build,
disableOptimizations: gbo.disableOptimizations,
mod: gbo.mod,
}, nil
}

// https://golang.org/pkg/cmd/go/internal/modinfo/#ModulePublic
type modInfo struct {
Path string
Dir string
}

// moduleInfo returns the module path and module root directory for a project
// using go modules, otherwise returns nil.
//
// Related: https://github.com/golang/go/issues/26504
func moduleInfo() *modInfo {
output, err := exec.Command("go", "list", "-mod=readonly", "-m", "-json").Output()
if err != nil {
return nil
}
var info modInfo
if err := json.Unmarshal(output, &info); err != nil {
return nil
}
return &info
}

// NewGo returns a build.Interface implementation that:
// 1. builds go binaries named by importpath,
// 2. containerizes the binary on a suitable base,
func NewGo(options ...Option) (Interface, error) {
gbo := &gobuildOpener{
build: build,
mod: moduleInfo(),
}

for _, option := range options {
Expand All @@ -90,14 +118,35 @@ func NewGo(options ...Option) (Interface, error) {
//
// Only valid importpaths that provide commands (i.e., are "package main") are
// supported.
func (*gobuild) IsSupportedReference(s string) bool {
p, err := gb.Import(s, gb.Default.GOPATH, gb.ImportComment)
func (g *gobuild) IsSupportedReference(s string) bool {
p, err := g.importPackage(s)
if err != nil {
return false
}
return p.IsCommand()
}

var moduleErr = errors.New("unmatched importPackage with gomodules")

// importPackage wraps go/build.Import to handle go modules.
//
// Note that we will fall back to GOPATH if the project isn't using go modules.
func (g *gobuild) importPackage(s string) (*gb.Package, error) {
if g.mod == nil {
return gb.Import(s, gb.Default.GOPATH, gb.ImportComment)
}

// If we're inside a go modules project, try to use the module's directory
// as our source root to import:
// * paths that match module path prefix (they should be in this project)
// * relative paths (they should also be in this project)
if strings.HasPrefix(s, g.mod.Path) || gb.IsLocalImport(s) {
return gb.Import(s, g.mod.Dir, gb.ImportComment)
}

return nil, moduleErr
}

func build(ip string, disableOptimizations bool) (string, error) {
tmpDir, err := ioutil.TempDir("", "ko")
if err != nil {
Expand Down Expand Up @@ -216,8 +265,8 @@ func tarBinary(name, binary string) (*bytes.Buffer, error) {
return buf, nil
}

func kodataPath(s string) (string, error) {
p, err := gb.Import(s, gb.Default.GOPATH, gb.ImportComment)
func (g *gobuild) kodataPath(s string) (string, error) {
p, err := g.importPackage(s)
if err != nil {
return "", err
}
Expand All @@ -227,7 +276,7 @@ func kodataPath(s string) (string, error) {
// Where kodata lives in the image.
const kodataRoot = "/var/run/ko"

func tarKoData(importpath string) (*bytes.Buffer, error) {
func (g *gobuild) tarKoData(importpath string) (*bytes.Buffer, error) {
buf := bytes.NewBuffer(nil)
// Compress this before calling tarball.LayerFromOpener, since it eagerly
// calculates digests and diffids. This prevents us from double compressing
Expand All @@ -239,7 +288,7 @@ func tarKoData(importpath string) (*bytes.Buffer, error) {
tw := tar.NewWriter(gw)
defer tw.Close()

root, err := kodataPath(importpath)
root, err := g.kodataPath(importpath)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -307,7 +356,7 @@ func (gb *gobuild) Build(s string) (v1.Image, error) {

var layers []mutate.Addendum
// Create a layer from the kodata directory under this import path.
dataLayerBuf, err := tarKoData(s)
dataLayerBuf, err := gb.tarKoData(s)
if err != nil {
return nil, err
}
Expand Down
42 changes: 42 additions & 0 deletions pkg/build/gobuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,48 @@ func TestGoBuildIsSupportedRef(t *testing.T) {
}
}

func TestGoBuildIsSupportedRefWithModules(t *testing.T) {
base, err := random.Image(1024, 3)
if err != nil {
t.Fatalf("random.Image() = %v", err)
}

mod := &modInfo{
Path: filepath.FromSlash("github.com/google/ko/cmd/ko/test"),
Dir: ".",
}

ng, err := NewGo(WithBaseImages(func(string) (v1.Image, error) { return base, nil }),
withModuleInfo(mod))
if err != nil {
t.Fatalf("NewGo() = %v", err)
}

// Supported import paths.
for _, importpath := range []string{
filepath.FromSlash("github.com/google/ko/cmd/ko/test"), // ko can build the test package.
} {
t.Run(importpath, func(t *testing.T) {
if !ng.IsSupportedReference(importpath) {
t.Errorf("IsSupportedReference(%q) = false, want true", importpath)
}
})
}

// Unsupported import paths.
for _, importpath := range []string{
filepath.FromSlash("github.com/google/ko/pkg/build"), // not a command.
filepath.FromSlash("github.com/google/ko/pkg/nonexistent"), // does not exist.
filepath.FromSlash("github.com/google/ko/cmd/ko"), // not in this module.
} {
t.Run(importpath, func(t *testing.T) {
if ng.IsSupportedReference(importpath) {
t.Errorf("IsSupportedReference(%v) = true, want false", importpath)
}
})
}
}

// A helper method we use to substitute for the default "build" method.
func writeTempFile(s string, _ bool) (string, error) {
tmpDir, err := ioutil.TempDir("", "ko")
Expand Down
13 changes: 12 additions & 1 deletion pkg/build/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,21 @@ func WithDisabledOptimizations() Option {
}

// withBuilder is a functional option for overriding the way go binaries
// are built. This is exposed for testing.
// are built.
// This is exposed for testing.
func withBuilder(b builder) Option {
return func(gbo *gobuildOpener) error {
gbo.build = b
return nil
}
}

// withModulePath is a functional option for overriding the module path for
// the current ko invocation.
// This is exposed for testing.
func withModuleInfo(mi *modInfo) Option {
return func(gbo *gobuildOpener) error {
gbo.mod = mi
return nil
}
}
33 changes: 15 additions & 18 deletions pkg/commands/publisher.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,34 @@ package commands
import (
"fmt"
gb "go/build"
"os"
"path/filepath"
"strings"

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

"golang.org/x/tools/go/packages"
)

func qualifyLocalImport(importpath, gopathsrc, pwd string) (string, error) {
if !strings.HasPrefix(pwd, gopathsrc) {
return "", fmt.Errorf("pwd (%q) must be on $GOPATH/src (%q) to support local imports", pwd, gopathsrc)
func qualifyLocalImport(importpath string) (string, error) {
cfg := &packages.Config{
Mode: packages.NeedName,
}
pkgs, err := packages.Load(cfg, importpath)
if err != nil {
return "", err
}
if len(pkgs) != 1 {
return "", fmt.Errorf("found %d local packages, expected 1", len(pkgs))
}
// Given $GOPATH/src and $PWD (which must be within $GOPATH/src), trim
// off $GOPATH/src/ from $PWD and append local importpath to get the
// fully-qualified importpath.
return filepath.Join(strings.TrimPrefix(pwd, gopathsrc+string(filepath.Separator)), importpath), nil
return pkgs[0].PkgPath, nil
}

func publishImages(importpaths []string, pub publish.Interface, b build.Interface) (map[string]name.Reference, error) {
imgs := make(map[string]name.Reference)
for _, importpath := range importpaths {
if gb.IsLocalImport(importpath) {
// Qualify relative imports to their fully-qualified
// import path, assuming $PWD is within $GOPATH/src.
gopathsrc := filepath.Join(gb.Default.GOPATH, "src")
pwd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("error getting current working directory: %v", err)
}
importpath, err = qualifyLocalImport(importpath, gopathsrc, pwd)
var err error
importpath, err = qualifyLocalImport(importpath)
if err != nil {
return nil, err
}
Expand Down
48 changes: 0 additions & 48 deletions pkg/commands/publisher_test.go

This file was deleted.

3 changes: 3 additions & 0 deletions vendor/golang.org/x/tools/AUTHORS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions vendor/golang.org/x/tools/CONTRIBUTORS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions vendor/golang.org/x/tools/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 3566d3f

Please sign in to comment.