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

Jib Auto Sync - core + maven implementation #3369

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions pkg/skaffold/build/jib/jib.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ type filesLists struct {
// watchedFiles maps from project name to watched files
var watchedFiles = map[string]filesLists{}

func GetBuildDefinitions(a *latest.JibArtifact) []string {
return watchedFiles[a.Project].BuildDefinitions
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect a.Project needs to be combined with the artifact context. As @chanseokoh noted below, a.Project may be the empty string, and it's entirely possible to have two artifacts with different contexts (workspaces) and same project name.

}

// GetDependencies returns a list of files to watch for changes to rebuild
func GetDependencies(ctx context.Context, workspace string, artifact *latest.JibArtifact) ([]string, error) {
t, err := DeterminePluginType(workspace, artifact)
Expand Down
213 changes: 213 additions & 0 deletions pkg/skaffold/build/jib/jib_sync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/*
Copyright 2019 The Skaffold Authors

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 jib

import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"time"

"github.com/GoogleContainerTools/skaffold/pkg/skaffold/filemon"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
"github.com/pkg/errors"
)

var syncLists = make(map[string]SyncMap)

type SyncMap struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add some go doc to describe that this structure is filled in from information from the jib builder (mvn jib:_.../gradle _jib...). I have to admit that Direct didn't have obvious semantics, but I don't have anything better to suggest.

Direct []SyncEntry `json:"direct"`
Generated []SyncEntry `json:"generated"`
}

type SyncEntry struct {
Src string `json:"src"`
Dest string `json:"dest"`

filetime time.Time
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does an entry always refer to a file and not a directory? Just to understand what the modification time of a directory may mean, if it can be a directory. But I kind of remember that our sync map will list every single file and not directories?

And I always forget. For a Java file, will the entry be a .java file or a .class file?

}

func InitSync(ctx context.Context, workspace string, artifact *latest.JibArtifact) error {
syncMap, err := getSyncMap(ctx, workspace, artifact)
if (err != nil) {
return err
}
syncLists[artifact.Project] = *syncMap
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that multiple Java projects have the same "JibArtifact.Project" name as a key to the map? The map is a global map, isn't it?

And isn't artifact.Project == "" for a single-module project setup?

return nil
}

// returns toCopy, toDelete, error
func GetSyncDiff(ctx context.Context, workspace string, artifact *latest.JibArtifact, e filemon.Events) (map[string][]string, map[string][]string, error) {
Comment on lines +59 to +60
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could consider using named return values to make this more explicit:

func GetSyncDiff(...) (toCopy map[string][]string, toDelete map[string][]string, err error) {

Or maybe it'd be better to return an explicit struct SyncDiff?


// if anything that was modified was a buildfile, do NOT sync, do a rebuild
buildFiles := GetBuildDefinitions(artifact)
for _, f := range e.Modified {
if !filepath.IsAbs(f) {
if ff, err := filepath.Abs(f); err != nil{
return nil, nil, err
} else {
f = ff
}
}
for _, bf := range buildFiles {
if f == bf {
return nil, nil, nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering the method name, I think it's worth adding a comment nil, nil means a full rebuild.

}
}
}

// it seems less than efficient to keep the original JSON structure when doing look ups, so maybe we should only use the json objects for serialization
// and store the sync data in a better type?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original format leads to O(n) checks

oldSyncMap := syncLists[artifact.Project]


// if all files are modified and direct, we don't need to build anything
if len(e.Deleted) == 0 || len(e.Added) == 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

|| seems at odd with the comment? And we could skip this if len(e.Modified) > len(oldSyncMap.Direct)?

matches := make(map[string][]string)
for _, f := range e.Modified {
for _, se := range oldSyncMap.Direct {
// filemon.Events doesn't seem to make any guarantee about the paths,
// so convert them to absolute paths (that's what jib provides)
if !filepath.IsAbs(f) {
if ff, err := filepath.Abs(f); err != nil{
return nil, nil, err
} else {
f = ff
}
}
Comment on lines +91 to +97
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be moved to the outer for loop

if se.Src == f {
matches[se.Src] = []string{se.Dest}
break
}
}
}
if len(matches) == len(e.Modified) {
return matches, nil, nil
}
}

if len(e.Deleted) != 0 {
// change into logging
fmt.Println("Deletions are not supported by jib auto sync at the moment")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Println("Deletions are not supported by jib auto sync at the moment")
fmt.Println("Deletions are not yet supported by jib auto sync")

return nil, nil, nil;
}

// we need to do another build and get a new sync map
newSyncMap, err := getSyncMap(ctx, workspace, artifact)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, so getSyncMap is really supposed to trigger a build itself (i.e., doing package)?

I'd like to understand how this could potentially interfere (race condition) with normal file watching, as we talked about before.

if err != nil {
return nil, nil, err;
}
syncLists[artifact.Project] = *newSyncMap

toCopy := make(map[string][]string)
// calculate the diff of the syncmaps
// known: this doesn't handle the case that something in the oldSyncMap is
// no longer represented in the new sync map
for _, se := range newSyncMap.Generated {
for _, seOld := range oldSyncMap.Generated {
if se.Src == seOld.Src && !se.filetime.Equal(seOld.filetime) {
toCopy[se.Src] = []string{se.Dest}
}
}
}
for _, se := range newSyncMap.Direct {
for _, seOld := range oldSyncMap.Direct {
if se.Src == seOld.Src && !se.filetime.Equal(seOld.filetime) {
toCopy[se.Src] = []string{se.Dest}
}
}
}

return toCopy, nil, nil
}

// getSyncMap returns a list of files that can be sync'd to a remote container
func getSyncMap(ctx context.Context, workspace string, artifact *latest.JibArtifact) (*SyncMap, error) {

// cmd will hold context that identifies the project
cmd, err := getSyncMapCommand(ctx, workspace, artifact)
if err != nil {
return nil, errors.WithStack(err)
}

projectSyncMap := SyncMap{}
if err = runAndParseSyncMap(cmd, &projectSyncMap); err != nil {
return nil, errors.WithStack(err)
}


// store the filetimes for all these values
if err := updateModTime(projectSyncMap.Direct); err != nil {
return nil, errors.WithStack(err)
}
if err := updateModTime(projectSyncMap.Generated); err != nil {
return nil, errors.WithStack(err)
}

return &projectSyncMap, nil
}

func getSyncMapCommand(ctx context.Context, workspace string, artifact *latest.JibArtifact) (*exec.Cmd, error) {
t, err := DeterminePluginType(workspace, artifact)
if err != nil {
return nil, err
}

switch t {
case JibMaven:
return getSyncMapCommandMaven(ctx, workspace, artifact), nil
case JibGradle:
return nil, errors.Errorf("unable to sync gradle projects at %s", workspace)
default:
return nil, errors.Errorf("unable to determine Jib builder type for %s", workspace)
}
}

func runAndParseSyncMap(cmd *exec.Cmd, sm *SyncMap) error {
stdout, err := util.RunCmdOut(cmd)
if err != nil {
return errors.Wrap(err, "failed to get Jib sync map")
}

// To parse the output, search for "BEGIN JIB JSON", then unmarshal the next line into the pathMap struct.
matches := regexp.MustCompile(`BEGIN JIB JSON\r?\n({.*})`).FindSubmatch(stdout)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked @briandealwis's idea to have this extensible. Something we should keep in mind to make things match.

if len(matches) == 0 {
return errors.New("failed to get Jib Sync data")
}

line := bytes.Replace(matches[1], []byte(`\`), []byte(`\\`), -1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line throws me every time I come across it. I believe this replace is required is because of Windows paths? Should we instead fix our emitting code in Jib?

return json.Unmarshal(line, &sm)
}

func updateModTime(se []SyncEntry) error {
for i, _ := range se {
e := &se[i]
if info, err := os.Stat(e.Src); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope this (and the operation to compute absolute path above) is cheap to do. The sync map will inherently be small, so we should probably good?

return errors.Wrap(err, "jib could not get filetime data")
} else {
e.filetime = info.ModTime();
}
}
return nil
}

23 changes: 16 additions & 7 deletions pkg/skaffold/build/jib/maven.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,24 @@ func getCommandMaven(ctx context.Context, workspace string, a *latest.JibArtifac
return MavenCommand.CreateCommand(ctx, workspace, args)
}

func getSyncMapCommandMaven(ctx context.Context, workspace string, a *latest.JibArtifact) *exec.Cmd {
cmd := MavenCommand.CreateCommand(ctx, workspace, mavenBuildArgs("_skaffold-sync-map", a, true))
Copy link
Member

@chanseokoh chanseokoh Dec 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mavenBuildArgs append either package or prepare-package. Does that make sense? And I don't really think _skaffold-sync-map will work. It should be jib:_skaffold-sync-map?

Also we pass --quiet for _skaffold-files_v2. But I'm not sure if --quiet helps, because I didn't see any Maven output in your short demo.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, forget prepending jib:_. I see it's added in mavenBuildArgs.

return &cmd
}

// GenerateMavenArgs generates the arguments to Maven for building the project as an image.
func GenerateMavenArgs(goal string, imageName string, a *latest.JibArtifact, skipTests bool, insecureRegistries map[string]bool) []string {
args := mavenBuildArgs(goal, a, skipTests)
if insecure, err := isOnInsecureRegistry(imageName, insecureRegistries); err == nil && insecure {
// jib doesn't support marking specific registries as insecure
args = append(args, "-Djib.allowInsecureRegistries=true")
}
args = append(args, "-Dimage="+imageName)

return args
}

func mavenBuildArgs(goal string, a *latest.JibArtifact, skipTests bool) []string {
// disable jib's rich progress footer on builds; we could use --batch-mode
// but it also disables colour which can be helpful
args := []string{"-Djib.console=plain"}
Expand All @@ -103,13 +119,6 @@ func GenerateMavenArgs(goal string, imageName string, a *latest.JibArtifact, ski
// multi-module project: instruct jib to containerize only the given module
args = append(args, "package", "jib:"+goal, "-Djib.containerize="+a.Project)
}

if insecure, err := isOnInsecureRegistry(imageName, insecureRegistries); err == nil && insecure {
// jib doesn't support marking specific registries as insecure
args = append(args, "-Djib.allowInsecureRegistries=true")
}
args = append(args, "-Dimage="+imageName)

return args
}

Expand Down
7 changes: 6 additions & 1 deletion pkg/skaffold/runner/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func (r *SkaffoldRunner) Dev(ctx context.Context, out io.Writer, artifacts []*la
},
func(e filemon.Events) {
syncMap := func() (map[string][]string, error) { return r.builder.SyncMap(ctx, artifact) }
s, err := sync.NewItem(artifact, e, r.builds, r.runCtx.InsecureRegistries, syncMap)
s, err := sync.NewItem(ctx, artifact, e, r.builds, r.runCtx.InsecureRegistries, syncMap)
switch {
case err != nil:
logrus.Warnf("error adding dirty artifact to changeset: %s", err.Error())
Expand Down Expand Up @@ -190,6 +190,11 @@ func (r *SkaffoldRunner) Dev(ctx context.Context, out io.Writer, artifacts []*la

logrus.Infoln("List generated in", time.Since(start))

// Init Sync State
if err := sync.Init(ctx, artifacts); err != nil {
return errors.Wrap(err, "exiting dev mode because initializing sync state failed")
}

// First build
if _, err := r.BuildAndTest(ctx, out, artifacts); err != nil {
return errors.Wrap(err, "exiting dev mode because first build failed")
Expand Down
7 changes: 7 additions & 0 deletions pkg/skaffold/schema/latest/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,9 @@ type Sync struct {
// The container destination is inferred by the builder.
// Currently only available for docker artifacts.
Infer []string `yaml:"infer,omitempty" yamltags:"oneOf=sync"`

// Auto delegate discovery of sync rules to the build system.
Auto *Auto `yaml:"auto,omitempty" yamltags:"oneOf=sync"`
}

// SyncRule specifies which local files to sync to remote folders.
Expand All @@ -600,6 +603,10 @@ type SyncRule struct {
Strip string `yaml:"strip,omitempty"`
}

// Auto may be extended to allow customization of the `auto` block but is currently
// empty
type Auto struct {}

// Profile is used to override any `build`, `test` or `deploy` configuration.
type Profile struct {
// Name is a unique profile name.
Expand Down
44 changes: 43 additions & 1 deletion pkg/skaffold/sync/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"path/filepath"
"strings"

jib2 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/jib"
"github.com/bmatcuk/doublestar"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
Expand All @@ -44,7 +45,7 @@ var (
WorkingDir = docker.RetrieveWorkingDir
)

func NewItem(a *latest.Artifact, e filemon.Events, builds []build.Artifact, insecureRegistries map[string]bool, destProvider DestinationProvider) (*Item, error) {
func NewItem(ctx context.Context, a *latest.Artifact, e filemon.Events, builds []build.Artifact, insecureRegistries map[string]bool, destProvider DestinationProvider) (*Item, error) {
if !e.HasChanged() || a.Sync == nil {
return nil, nil
}
Expand All @@ -57,6 +58,10 @@ func NewItem(a *latest.Artifact, e filemon.Events, builds []build.Artifact, inse
return inferredSyncItem(a, e, builds, destProvider)
}

if a.Sync.Auto != nil {
return autoSyncItem(ctx, a, e, builds)
}

return nil, nil
}

Expand Down Expand Up @@ -138,6 +143,28 @@ func inferredSyncItem(a *latest.Artifact, e filemon.Events, builds []build.Artif
return &Item{Image: tag, Copy: toCopy}, nil
}

func autoSyncItem(ctx context.Context, a *latest.Artifact, e filemon.Events, builds []build.Artifact) (*Item, error) {
tag := latestTag(a.ImageName, builds)
if tag == "" {
return nil, fmt.Errorf("could not find latest tag for image %s in builds: %v", a.ImageName, builds)
}

if e.HasChanged() {
if a.JibArtifact != nil {
if toCopy, toDelete, err := jib2.GetSyncDiff(ctx, a.Workspace, a.JibArtifact, e); err != nil {
return nil, err
} else if toCopy == nil && toDelete == nil {
// do a rebuild
return nil, nil
} else {
return &Item{Image: tag, Copy: toCopy, Delete: toDelete}, nil
}
}
}

return nil, nil
}

func latestTag(image string, builds []build.Artifact) string {
for _, build := range builds {
if build.ImageName == image {
Expand Down Expand Up @@ -260,3 +287,18 @@ func Perform(ctx context.Context, image string, files syncMap, cmdFn func(contex

return errs.Wait()
}

func Init(ctx context.Context, artifacts []*latest.Artifact) error {
for i := range artifacts {
a := artifacts[i]
if a.Sync != nil && a.Sync.Auto != nil{
if a.JibArtifact != nil {
if err := jib2.InitSync(ctx, a.Workspace, a.JibArtifact); err != nil {
// maybe should just disable sync here, instead of dead?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// maybe should just disable sync here, instead of dead?
// maybe should just disable sync here, instead of dying?

return errors.Wrapf(err, "error initializing sync state for %s", a.ImageName)
}
}
}
}
return nil
}