Skip to content

Commit

Permalink
Allow images to be loaded into kind using 'kind.local'. (ko-build#180)
Browse files Browse the repository at this point in the history
* Allow images to be loaded into kind using 'kind.local'.

* Add documentation for kind.
  • Loading branch information
markusthoemmes authored Sep 4, 2020
1 parent b7b0435 commit 1aa3b37
Show file tree
Hide file tree
Showing 334 changed files with 62,109 additions and 37,592 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ However, these same commands can be directed to operate locally as well via the
`--local` or `-L` command (or setting `KO_DOCKER_REPO=ko.local`). See the
[`minikube` section](./README.md#with-minikube) for more detail.

`ko` can also be used with `kind` directly by setting
`KO_DOCKER_REPO=kind.local`. See [the relevant section](./README.md#with-kind)
for more detail.

### `ko publish`

`ko publish` simply builds and publishes images for each import path passed as
Expand Down Expand Up @@ -311,6 +315,33 @@ Images will appear in the Docker daemon as
`ko.local/import.path.com/foo/cmd/bar`. With `--local` import paths are always
preserved (see `--preserve-import-paths`).

## With `kind`

Likewise, you can use `ko` with `kind` to aid in rapid local iteration both
locally and in small CI environments. To instruct `ko` to publish images into
your `kind` cluster, the `KO_DOCKER_REPO` variable must be set to `kind.local`.

This would look something like:

```shell
# Create a kind cluster
kind create cluster
# Deploy to minikube w/o registry.
KO_DOCKER_REPO=kind.local ko apply -L -f config/
```

Like with `minikube` above, a caveat of this approach is that it will not work
if your container is configured with `imagePullPolicy: Always` because despite
having the image locally, a pull is performed to ensure we have the latest
version, it still exists, and that access hasn't been revoked. A workaround for
this is to use `imagePullPolicy: IfNotPresent`, which should work well with `ko`
in all contexts.

Note that images will not appear in the Docker daemon running `kind` as the
cluster itself is running in a container that is running `containerd` inside.
The images are loaded into the respective `containerd` daemon.

## Configuration via `.ko.yaml`

While `ko` aims to have zero configuration, there are certain scenarios where
Expand Down
9 changes: 8 additions & 1 deletion cmd/ko/test/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import (
"io/ioutil"
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
)

func main() {
Expand All @@ -35,5 +37,10 @@ func main() {
if err != nil {
log.Fatalf("Error reading %q: %v", file, err)
}
log.Print(string(bytes))
log.Printf(string(bytes))

// Cause the pod to "hang" to allow us to check for a readiness state.
sigs := make(chan os.Signal)
signal.Notify(sigs, syscall.SIGTERM)
<-sigs
}
19 changes: 12 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,21 @@ require (
github.com/google/go-containerregistry v0.0.0-20200310013544-4fe717a9b4cb
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
github.com/mattmoor/dep-notify v0.0.0-20190205035814-a45dec370a17
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/cobra v0.0.5
github.com/spf13/cobra v1.0.0
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.3.2
github.com/spf13/viper v1.4.0
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/tools v0.0.0-20200210192313-1ace956b0e17
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d
gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71
gotest.tools/v3 v3.0.2 // indirect
k8s.io/apimachinery v0.17.1
k8s.io/cli-runtime v0.17.0
k8s.io/client-go v0.17.1 // indirect
k8s.io/apimachinery v0.18.2
k8s.io/cli-runtime v0.17.1
sigs.k8s.io/kind v0.8.1
)

replace (
k8s.io/apimachinery => k8s.io/apimachinery v0.17.1
k8s.io/cli-runtime => k8s.io/cli-runtime v0.17.1
k8s.io/client-go => k8s.io/client-go v0.17.1
)
56 changes: 51 additions & 5 deletions go.sum

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion integration_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ ROOT_DIR="$(pwd)"

echo "Running smoke test."
go install ./cmd/ko
ko apply -f ./cmd/ko/test -L
# Travis runs this against a kind cluster so properly use the kind publisher.
KO_DOCKER_REPO=kind.local ko apply -f ./cmd/ko/test
kubectl wait --timeout=10s --for=condition=Ready pod/kodata

echo "Moving GOPATH into /tmp/ to test modules behavior."
export ORIGINAL_GOPATH="$GOPATH"
Expand Down
3 changes: 3 additions & 0 deletions pkg/commands/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ func makePublisher(po *options.PublishOptions) (publish.Interface, error) {
// not be true.
return publish.NewDaemon(namer, po.Tags), nil
}
if repoName == publish.KindDomain {
return publish.NewKindPublisher(namer, po.Tags), nil
}

if repoName == "" {
return nil, errors.New("KO_DOCKER_REPO environment variable is unset")
Expand Down
86 changes: 86 additions & 0 deletions pkg/publish/kind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2020 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 (
"fmt"
"log"
"strings"

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

const (
// KindDomain is a sentinel "registry" that represents side-loading images into kind nodes.
KindDomain = "kind.local"
)

type kindPublisher struct {
namer Namer
tags []string
}

// NewKindPublisher returns a new publish.Interface that loads images into kind nodes.
func NewKindPublisher(namer Namer, tags []string) Interface {
return &kindPublisher{
namer: namer,
tags: tags,
}
}

// Publish implements publish.Interface.
func (t *kindPublisher) Publish(img v1.Image, s string) (name.Reference, error) {
s = strings.TrimPrefix(s, build.StrictScheme)
// https://github.com/google/go-containerregistry/issues/212
s = strings.ToLower(s)

h, err := img.Digest()
if err != nil {
return nil, err
}

digestTag, err := name.NewTag(fmt.Sprintf("%s/%s:%s", KindDomain, t.namer(s), h.Hex))
if err != nil {
return nil, err
}

log.Printf("Loading %v", digestTag)
if err := kind.Write(digestTag, img); err != nil {
return nil, err
}
log.Printf("Loaded %v", digestTag)

for _, tagName := range t.tags {
log.Printf("Adding tag %v", tagName)
tag, err := name.NewTag(fmt.Sprintf("%s/%s:%s", KindDomain, t.namer(s), tagName))
if err != nil {
return nil, err
}

if err := kind.Tag(digestTag, tag); err != nil {
return nil, err
}
log.Printf("Added tag %v", tagName)
}

return &digestTag, nil
}

func (t *kindPublisher) Close() error {
return nil
}
16 changes: 16 additions & 0 deletions pkg/publish/kind/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2020 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 kind defines methods for publishing images into kind nodes.
package kind
113 changes: 113 additions & 0 deletions pkg/publish/kind/write.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2020 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 kind

import (
"fmt"
"io"
"os"

"golang.org/x/sync/errgroup"

"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/tarball"

"sigs.k8s.io/kind/pkg/cluster"
"sigs.k8s.io/kind/pkg/cluster/nodes"
)

// Supported since kind 0.8.0 (https://github.com/kubernetes-sigs/kind/releases/tag/v0.8.0)
const clusterNameEnvKey = "KIND_CLUSTER_NAME"

// provider is an interface for kind providers to facilitate testing.
type provider interface {
ListInternalNodes(name string) ([]nodes.Node, error)
}

// GetProvider is a variable so we can override in tests.
var GetProvider = func() provider {
return cluster.NewProvider()
}

// Tag adds a tag to an already existent image.
func Tag(src, dest name.Tag) error {
return onEachNode(func(n nodes.Node) error {
cmd := n.Command("ctr", "--namespace=k8s.io", "images", "tag", "--force", src.String(), dest.String())
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to tag image: %w", err)
}
return nil
})
}

// Write saves the image into the kind nodes as the given tag.
func Write(tag name.Tag, img v1.Image) error {
return onEachNode(func(n nodes.Node) error {
pr, pw := io.Pipe()

grp := errgroup.Group{}
grp.Go(func() error {
return pw.CloseWithError(tarball.Write(tag, img, pw))
})

cmd := n.Command("ctr", "--namespace=k8s.io", "images", "import", "-").SetStdin(pr)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to load image to node %q: %w", n, err)
}

if err := grp.Wait(); err != nil {
return fmt.Errorf("failed to write intermediate tarball representation: %w", err)
}

return nil
})
}

// onEachNode executes the given function on each node. Exits on first error.
func onEachNode(f func(nodes.Node) error) error {
nodeList, err := getNodes()
if err != nil {
return err
}

for _, n := range nodeList {
if err := f(n); err != nil {
return err
}
}
return nil
}

// getNodes gets all the nodes of the default cluster.
// Returns an error if none were found.
func getNodes() ([]nodes.Node, error) {
provider := GetProvider()

clusterName := os.Getenv(clusterNameEnvKey)
if clusterName == "" {
clusterName = cluster.DefaultName
}

nodeList, err := provider.ListInternalNodes(clusterName)
if err != nil {
return nil, err
}
if len(nodeList) == 0 {
return nil, fmt.Errorf("no nodes found for cluster %q", cluster.DefaultName)
}

return nodeList, nil
}
Loading

0 comments on commit 1aa3b37

Please sign in to comment.