Skip to content
This repository has been archived by the owner on Feb 6, 2024. It is now read-only.

Commit

Permalink
Complete the implementation of the Go resolver (#435)
Browse files Browse the repository at this point in the history
* Implement YAML walker for resolver

* Return resolved yaml

* Fully implement resolver & add unit test for walker

* Undo local test edit

* buildifier

* Add grpc go deps for tests

* Validate tags using strict validation

* Move deleting seen images to top of resolver.

* Update e2e test to use v2_2 image instead of v2 image.
  • Loading branch information
smukherj1 committed Oct 11, 2019
1 parent fba396b commit d95cf52
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 8 deletions.
24 changes: 24 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,27 @@ bind(
)

# gazelle:repo bazel_gazelle

# Go dependencies needed for rules_k8s tests only.
load("@bazel_gazelle//:deps.bzl", "go_repository")

go_repository(
name = "org_golang_google_grpc",
importpath = "google.golang.org/grpc",
sum = "h1:q4XQuHFC6I28BKZpo6IYyb3mNO+l7lSOxRuYTCiDfXk=",
version = "v1.23.1",
)

go_repository(
name = "org_golang_x_net",
importpath = "golang.org/x/net",
sum = "h1:h5tBRKZ1aY/bo6GNqe/4zWC8GkaLOFQ5wPKIOQ0i2sA=",
version = "v0.0.0-20190918130420-a8b05e9114ab",
)

go_repository(
name = "org_golang_x_text",
importpath = "golang.org/x/text",
sum = "h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=",
version = "v0.3.2",
)
2 changes: 1 addition & 1 deletion examples/hellohttp/static-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ spec:
containers:
- name: hello-http
# Pick a public image that's unlikely to become unavailable.
image: gcr.io/google-containers/pause:2.0
image: gcr.io/google-containers/pause:3.1
imagePullPolicy: Always
ports:
- containerPort: 8080
10 changes: 9 additions & 1 deletion k8s/go/cmd/resolver/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")

package(default_visibility = ["//visibility:private"])

Expand All @@ -27,6 +27,7 @@ go_library(
"@com_github_google_go_containerregistry//pkg/authn:go_default_library",
"@com_github_google_go_containerregistry//pkg/name:go_default_library",
"@com_github_google_go_containerregistry//pkg/v1/remote:go_default_library",
"@in_gopkg_yaml_v2//:go_default_library",
"@io_bazel_rules_docker//container/go/pkg/compat:go_default_library",
"@io_bazel_rules_docker//container/go/pkg/utils:go_default_library",
],
Expand All @@ -37,3 +38,10 @@ go_binary(
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)

go_test(
name = "go_default_test",
srcs = ["resolver_test.go"],
embed = [":go_default_library"],
deps = ["@com_github_google_go_cmp//cmp:go_default_library"],
)
164 changes: 163 additions & 1 deletion k8s/go/cmd/resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"path"
"strings"
Expand All @@ -25,6 +26,7 @@ import (
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"gopkg.in/yaml.v2"
)

var (
Expand Down Expand Up @@ -186,6 +188,155 @@ func publish(spec []imageSpec, stamper *compat.Stamper) (map[string]string, map[
return overrides, unseen, nil
}

// resolver implements walking over arbitrary k8s YAML templates and
// transforming every string in the YAML with a configured string resolver.
type resolver struct {
// resolvedImages is a map from the tagged image name to the fully qualified
// image name by sha256 digest.
resolvedImages map[string]string
// unseen is the set of images that haven't been seen yet. Image names
// encountered in the k8s YAML template are removed from this set.
unseen map[string]bool
// strResolver is called to resolve every individual string encountered in
// the k8s YAML template. The functor interface allows mocking the string
// resolver in unit tests.
strResolver func(*resolver, string) (string, error)
}

// resolveString resolves a string found in the k8s YAML template by replacing
// a tagged image name with an image name referenced by its sha256 digest. If
// the given string doesn't represent a tagged image, it is returned as is.
// The given resolver is also modified:
// 1. If the given string was a tagged image, the resolved image lookup in the
// given resolver is updated to include a mapping from the given string to
// the resolved image name.
// 2. If the given string was a tagged image, the set of unseen images in the
// given resolver is updated to exclude the given string.
func resolveString(r *resolver, s string) (string, error) {
if _, ok := r.unseen[s]; ok {
delete(r.unseen, s)
}
o, ok := r.resolvedImages[s]
if ok {
return o, nil
}
t, err := name.NewTag(s, name.StrictValidation)
if err != nil {
// Silently ignore strings that can't be parsed as tagged image
// referneces.
return s, nil
}
auth, err := authn.DefaultKeychain.Resolve(t.Context())
if err != nil {
return "", fmt.Errorf("unable to get the authenticator using the default keychain for image %v: %v", t, auth)
}
img, err := remote.Image(t, remote.WithAuth(auth))
if err != nil {
return "", fmt.Errorf("unable to get image %v from registry: %v", t, err)
}
d, err := img.Digest()
if err != nil {
return "", fmt.Errorf("unable to get digest for image %v: %v", t, err)
}
resolved := fmt.Sprintf("%s/%s@%v", t.Context().RegistryStr(), t.Context().RepositoryStr(), d)
r.resolvedImages[s] = resolved
return resolved, nil
}

// resolveItem resolves the given YAML object if it's a string or recursively
// walks into the YAML collection type.
func (r *resolver) resolveItem(i interface{}) (interface{}, error) {
if s, ok := i.(string); ok {
return r.strResolver(r, s)
}
if l, ok := i.([]interface{}); ok {
return r.resolveList(l)
}
if m, ok := i.(map[interface{}]interface{}); ok {
return r.resolveMap(m)
}
return i, nil
}

// resolveList recursively walks the given yaml list.
func (r *resolver) resolveList(l []interface{}) ([]interface{}, error) {
result := []interface{}{}
for _, i := range l {
o, err := r.resolveItem(i)
if err != nil {
return nil, err
}
result = append(result, o)
}
return result, nil
}

// resolveMap recursively walks the given yaml map.
func (r *resolver) resolveMap(m map[interface{}]interface{}) (map[interface{}]interface{}, error) {
result := make(map[interface{}]interface{})
for k, v := range m {
rk, err := r.resolveItem(k)
if err != nil {
return nil, err
}
rv, err := r.resolveItem(v)
if err != nil {
return nil, err
}
result[rk] = rv
}
return result, nil
}

// resolveYAML recursively walks the given blob of arbitrary YAML and calls
// the strResolver on each string in the YAML document.
func (r *resolver) resolveYAML(b []byte) ([]byte, error) {
var l []interface{}
lErr := yaml.Unmarshal(b, &l)
if lErr == nil {
o, err := r.resolveItem(l)
if err != nil {
return nil, err
}
return yaml.Marshal(o)
}
var m map[interface{}]interface{}
mErr := yaml.Unmarshal(b, &m)
if mErr == nil {
o, err := r.resolveMap(m)
if err != nil {
return nil, err
}
return yaml.Marshal(o)
}

return nil, fmt.Errorf("unable to parse given blob as a YAML list or map: %v %v", lErr, mErr)
}

// resolveTemplate resolves the given YAML template using the given mapping from
// tagged to fully qualified image names referenced by their digest and the
// set of image names that haven't been seen yet. The given set of unseen images
// is updated to exclude the image names encountered in the given template.
func resolveTemplate(templateFile string, resolvedImages map[string]string, unseen map[string]bool) error {
t, err := ioutil.ReadFile(templateFile)
if err != nil {
return fmt.Errorf("unable to open template file %q: %v", templateFile, err)
}

r := resolver{
resolvedImages: resolvedImages,
unseen: unseen,
strResolver: resolveString,
}

resolved, err := r.resolveYAML(t)
if err != nil {
return fmt.Errorf("unable to resolve YAML template %q: %v", templateFile, err)
}
fmt.Println(string(resolved))
return nil
}

func main() {
flag.Var(&imgSpecs, "image_spec", "Associative lists of the constitutent elements of a docker image.")
flag.Var(&stampInfoFile, "stamp-info-file", "One or more Bazel stamp info files.")
Expand All @@ -204,7 +355,18 @@ func main() {
}
specs = append(specs, spec)
}
if _, _, err := publish(specs, stamper); err != nil {
resolvedImages, unseen, err := publish(specs, stamper)
if err != nil {
log.Fatalf("Unable to publish images: %v", err)
}
if err := resolveTemplate(*k8sTemplate, resolvedImages, unseen); err != nil {
log.Fatalf("Unable to resolve template file %q: %v", *k8sTemplate, err)
}
if len(unseen) > 0 && !*allowUnusedImages {
log.Printf("The following images given as --image_spec were not found in the template:")
for i := range unseen {
log.Printf("%s", i)
}
log.Fatalf("--allow_unused_images can be specified to ignore this error.")
}
}
50 changes: 50 additions & 0 deletions k8s/go/cmd/resolver/resolver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
)

// TestYAMLWalk ensures the YAML walker in the resolver visits all strings in
// an arbitrary YAML teplate with nested maps & lists.
func TestYAMLWalk(t *testing.T) {
y := []byte(`val1: val2
val3: val4
val5:
val6: val7
val8:
val9: val10
val11:
val12:
val13:
val14: val15
val16:
val17:
- val18: val19
val20: val21
val22: val23
val24:
- val25: val26
`)
got := make(map[string]bool)
sr := func(r *resolver, s string) (string, error) {
got[s] = true
return s, nil
}
r := &resolver{
strResolver: sr,
}
if _, err := r.resolveYAML(y); err != nil {
t.Fatalf("Failed to resolve YAML: %v", err)
}
want := make(map[string]bool)
for i := 1; i <= 26; i++ {
want[fmt.Sprintf("val%d", i)] = true
}

if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("YAML walker did not visit all strings (-want +got):\n%s", diff)
}
}
8 changes: 5 additions & 3 deletions k8s/k8s.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ py_library(
"@io_bazel_rules_k8s//toolchains/kubectl:kubectl_osx_toolchain",
"@io_bazel_rules_k8s//toolchains/kubectl:kubectl_windows_toolchain",
)

if "io_bazel_rules_go" not in excludes:
http_archive(
name = "io_bazel_rules_go",
sha256 = "f04d2373bcaf8aa09bccb08a98a57e721306c8f6043a2a0ee610fd6853dcde3d",
url = "https://github.com/bazelbuild/rules_go/releases/download/0.18.6/rules_go-0.18.6.tar.gz",
sha256 = "ae8c36ff6e565f674c7a3692d6a9ea1096e4c1ade497272c2108a810fb39acd2",
urls = [
"https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/rules_go/releases/download/0.19.4/rules_go-0.19.4.tar.gz",
"https://github.com/bazelbuild/rules_go/releases/download/0.19.4/rules_go-0.19.4.tar.gz",
],
)
if "bazel_gazelle" not in excludes:
http_archive(
Expand Down
12 changes: 11 additions & 1 deletion k8s/k8s_go_deps.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.
"""Macro to load Go package dependencies of Go binaries in this repository."""

load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
load(
"@io_bazel_rules_docker//repositories:go_repositories.bzl",
rules_docker_go_deps = "go_deps",
Expand All @@ -35,3 +35,13 @@ def deps():
gazelle_dependencies()
rules_docker_repositories()
rules_docker_go_deps()

excludes = native.existing_rules().keys()

if "com_github_google_go_cmp" not in excludes:
go_repository(
name = "com_github_google_go_cmp",
importpath = "github.com/google/go-cmp",
sum = "h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=",
version = "v0.3.1",
)
2 changes: 1 addition & 1 deletion k8s/object.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ _common_attrs = {
# Extra arguments to pass to the resolver.
"resolver_args": attr.string_list(),
"use_legacy_resolver": attr.bool(
default = True,
default = False,
doc = "Use the legacy python resolver if True. Use the experimental" +
" Go resolver if false.",
),
Expand Down

0 comments on commit d95cf52

Please sign in to comment.