Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add go module support #60

Merged
merged 4 commits into from
Jul 13, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

62 changes: 55 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,34 @@ 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()
}

// importPackage wraps go/build.Import to handle go modules.
//
// Note that we will fall back to using GOPATH if the project isn't using go
// modules or the import path doesn't match the module path of the project.
func (g *gobuild) importPackage(s string) (*gb.Package, error) {
if g.mod != nil {
// 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) {
pkg, err := gb.Import(s, g.mod.Dir, gb.ImportComment)
if err == nil {
return pkg, err
}
}
}
return gb.Import(s, gb.Default.GOPATH, gb.ImportComment)
}

func build(ip string, disableOptimizations bool) (string, error) {
tmpDir, err := ioutil.TempDir("", "ko")
if err != nil {
Expand Down Expand Up @@ -216,8 +264,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 +275,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 +287,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 +355,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
11 changes: 9 additions & 2 deletions pkg/build/gobuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,21 @@ func TestGoBuildIsSupportedRef(t *testing.T) {
t.Fatalf("random.Image() = %v", err)
}

ng, err := NewGo(WithBaseImages(func(string) (v1.Image, error) { return base, nil }))
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"), // ko can build itself.
filepath.FromSlash("github.com/google/ko/cmd/ko"), // ko can build itself.
filepath.FromSlash("github.com/google/ko/cmd/ko/test"), // ko can build the test package.
jonjohnsonjr marked this conversation as resolved.
Show resolved Hide resolved
} {
t.Run(importpath, func(t *testing.T) {
if !ng.IsSupportedReference(importpath) {
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.

22 changes: 22 additions & 0 deletions vendor/golang.org/x/tools/PATENTS

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

Loading