diff --git a/bin/kubeyaml b/bin/kubeyaml index 594ae6d98..2d5f8b756 100755 --- a/bin/kubeyaml +++ b/bin/kubeyaml @@ -1,2 +1,2 @@ #!/bin/sh -docker run --rm -i quay.io/squaremo/kubeyaml:0.3.3 "$@" +docker run --rm -i quay.io/squaremo/kubeyaml:0.4.2 "$@" diff --git a/cluster/kubernetes/resource/fluxhelmrelease.go b/cluster/kubernetes/resource/fluxhelmrelease.go index b51de298d..d4b20f75b 100644 --- a/cluster/kubernetes/resource/fluxhelmrelease.go +++ b/cluster/kubernetes/resource/fluxhelmrelease.go @@ -54,61 +54,127 @@ func sorted_keys(values map[string]interface{}) []string { // it cannot interpret the values as specifying images, or if the // `visit` function itself returns an error. func FindFluxHelmReleaseContainers(values map[string]interface{}, visit func(string, image.Ref, ImageSetter) error) error { - // Try the simplest format first: - // ``` - // values: - // image: 'repo/image:tag' - // ``` - if imgInfo, ok := values["image"]; ok { - if imgInfoStr, ok := imgInfo.(string); ok { - imageRef, err := image.ParseRef(imgInfoStr) - if err == nil { - return visit(ReleaseContainerName, imageRef, func(ref image.Ref) { - values["image"] = ref.String() - }) - } - } + // an image defined at the top-level is given a standard container name: + if image, setter, ok := interpretAsContainer(stringMap(values)); ok { + visit(ReleaseContainerName, image, setter) } - // Second most simple format: - // ``` - // values: - // foo: - // image: repo/foo:tag - // bar: - // image: repo/bar:tag - // ``` + + // an image as part of a field is treated as a "container" spec + // named for the field: for _, k := range sorted_keys(values) { - var imgInfo interface{} - var ok bool - var setter ImageSetter - // From a YAML (i.e., a file), it's a - // `map[interface{}]interface{}`, and from JSON (i.e., - // Kubernetes API) it's a `map[string]interface{}`. - switch m := values[k].(type) { - case map[string]interface{}: - imgInfo, ok = m["image"] - setter = func(ref image.Ref) { - m["image"] = ref.String() - } - case map[interface{}]interface{}: - imgInfo, ok = m["image"] - setter = func(ref image.Ref) { - m["image"] = ref.String() - } + if image, setter, ok := interpret(values[k]); ok { + visit(k, image, setter) } - if ok { - if imgInfoStr, ok := imgInfo.(string); ok { - imageRef, err := image.ParseRef(imgInfoStr) - if err == nil { - err = visit(k, imageRef, setter) + } + return nil +} + +// The following is some machinery for interpreting a +// FluxHelmRelease's `values` field as defining images to be +// interpolated into the chart templates. +// +// The top-level value is a map[string]interface{}, but beneath that, +// we get maps in two varieties: from a YAML (i.e., a file), they are +// `map[interface{}]interface{}`, and from JSON (i.e., Kubernetes API) +// they are a `map[string]interface{}`. To conflate them, here's an +// interface for maps: + +type mapper interface { + get(string) (interface{}, bool) + set(string, interface{}) +} + +type stringMap map[string]interface{} +type anyMap map[interface{}]interface{} + +func (m stringMap) get(k string) (interface{}, bool) { v, ok := m[k]; return v, ok } +func (m stringMap) set(k string, v interface{}) { m[k] = v } + +func (m anyMap) get(k string) (interface{}, bool) { v, ok := m[k]; return v, ok } +func (m anyMap) set(k string, v interface{}) { m[k] = v } + +// interpret gets a value which may contain a description of an image. +func interpret(values interface{}) (image.Ref, ImageSetter, bool) { + switch m := values.(type) { + case map[string]interface{}: + return interpretAsContainer(stringMap(m)) + case map[interface{}]interface{}: + return interpretAsContainer(anyMap(m)) + } + return image.Ref{}, nil, false +} + +// interpretAsContainer takes a `mapper` value that may _contain_ an +// image, and attempts to interpret it. +func interpretAsContainer(m mapper) (image.Ref, ImageSetter, bool) { + imageValue, ok := m.get("image") + if !ok { + return image.Ref{}, nil, false + } + switch img := imageValue.(type) { + case string: + // ``` + // container: + // image: 'repo/image:tag' + // ``` + imageRef, err := image.ParseRef(img) + if err == nil { + var taggy bool + if tag, ok := m.get("tag"); ok { + // conatainer: + // image: repo/foo + // tag: v1 + if tagStr, ok := tag.(string); ok { + taggy = true + imageRef.Tag = tagStr } - if err != nil { - return err + } + return imageRef, func(ref image.Ref) { + if taggy { + m.set("image", ref.Name.String()) + m.set("tag", ref.Tag) + return } + m.set("image", ref.String()) + }, true + } + case map[string]interface{}: + return interpretAsImage(stringMap(img)) + case map[interface{}]interface{}: + return interpretAsImage(anyMap(img)) + } + return image.Ref{}, nil, false +} + +// interpretAsImage takes a `mapper` value that may represent an +// image, and attempts to interpret it. +func interpretAsImage(m mapper) (image.Ref, ImageSetter, bool) { + var imgRepo, imgTag interface{} + var ok bool + if imgRepo, ok = m.get("repository"); !ok { + return image.Ref{}, nil, false + } + + if imgTag, ok = m.get("tag"); !ok { + return image.Ref{}, nil, false + } + + if imgStr, ok := imgRepo.(string); ok { + if tagStr, ok := imgTag.(string); ok { + // container: + // image: + // repository: repo/bar + // tag: v1 + imgRef, err := image.ParseRef(imgStr + ":" + tagStr) + if err == nil { + return imgRef, func(ref image.Ref) { + m.set("repository", ref.Name.String()) + m.set("tag", ref.Tag) + }, true } } } - return nil + return image.Ref{}, nil, false } // Containers returns the containers that are defined in the diff --git a/cluster/kubernetes/resource/fluxhelmrelease_test.go b/cluster/kubernetes/resource/fluxhelmrelease_test.go index bb93f9cd7..8cd037e6e 100644 --- a/cluster/kubernetes/resource/fluxhelmrelease_test.go +++ b/cluster/kubernetes/resource/fluxhelmrelease_test.go @@ -1,12 +1,13 @@ package resource import ( + "fmt" "testing" "github.com/weaveworks/flux/resource" ) -func TestParseSimpleFormat(t *testing.T) { +func TestParseImageOnlyFormat(t *testing.T) { expectedImage := "bitnami/mariadb:10.1.30-r1" doc := `--- apiVersion: helm.integrations.flux.weave.works/v1alpha2 @@ -19,6 +20,7 @@ metadata: spec: chartGitPath: mariadb values: + first: post image: ` + expectedImage + ` persistence: enabled: false @@ -47,7 +49,53 @@ spec: } } -func TestParseLessSimpleFormat(t *testing.T) { +func TestParseImageTagFormat(t *testing.T) { + expectedImageName := "bitnami/mariadb" + expectedImageTag := "10.1.30-r1" + expectedImage := expectedImageName + ":" + expectedImageTag + + doc := `--- +apiVersion: helm.integrations.flux.weave.works/v1alpha2 +kind: FluxHelmRelease +metadata: + name: mariadb + namespace: maria + labels: + chart: mariadb +spec: + chartGitPath: mariadb + values: + first: post + image: ` + expectedImageName + ` + tag: ` + expectedImageTag + ` + persistence: + enabled: false +` + + resources, err := ParseMultidoc([]byte(doc), "test") + if err != nil { + t.Fatal(err) + } + res, ok := resources["maria:fluxhelmrelease/mariadb"] + if !ok { + t.Fatalf("expected resource not found; instead got %#v", resources) + } + fhr, ok := res.(resource.Workload) + if !ok { + t.Fatalf("expected resource to be a Workload, instead got %#v", res) + } + + containers := fhr.Containers() + if len(containers) != 1 { + t.Errorf("expected 1 container; got %#v", containers) + } + image := containers[0].Image.String() + if image != expectedImage { + t.Errorf("expected container image %q, got %q", expectedImage, image) + } +} + +func TestParseNamedImageFormat(t *testing.T) { expectedContainer := "db" expectedImage := "bitnami/mariadb:10.1.30-r1" doc := `--- @@ -62,6 +110,7 @@ spec: chartGitPath: mariadb values: ` + expectedContainer + `: + first: post image: ` + expectedImage + ` persistence: enabled: false @@ -109,3 +158,264 @@ spec: t.Errorf("expected container name %q, got %q", expectedContainer, containers[0].Name) } } + +func TestParseNamedImageTagFormat(t *testing.T) { + expectedContainer := "db" + expectedImageName := "bitnami/mariadb" + expectedImageTag := "10.1.30-r1" + expectedImage := expectedImageName + ":" + expectedImageTag + + doc := `--- +apiVersion: helm.integrations.flux.weave.works/v1alpha2 +kind: FluxHelmRelease +metadata: + name: mariadb + namespace: maria + labels: + chart: mariadb +spec: + chartGitPath: mariadb + values: + other: + not: "containing image" + ` + expectedContainer + `: + first: post + image: ` + expectedImageName + ` + tag: ` + expectedImageTag + ` + persistence: + enabled: false +` + + resources, err := ParseMultidoc([]byte(doc), "test") + if err != nil { + t.Fatal(err) + } + res, ok := resources["maria:fluxhelmrelease/mariadb"] + if !ok { + t.Fatalf("expected resource not found; instead got %#v", resources) + } + fhr, ok := res.(resource.Workload) + if !ok { + t.Fatalf("expected resource to be a Workload, instead got %#v", res) + } + + containers := fhr.Containers() + if len(containers) != 1 { + t.Fatalf("expected 1 container; got %#v", containers) + } + image := containers[0].Image.String() + if image != expectedImage { + t.Errorf("expected container image %q, got %q", expectedImage, image) + } + if containers[0].Name != expectedContainer { + t.Errorf("expected container name %q, got %q", expectedContainer, containers[0].Name) + } + + newImage := containers[0].Image.WithNewTag("some-other-tag") + if err := fhr.SetContainerImage(expectedContainer, newImage); err != nil { + t.Error(err) + } + + containers = fhr.Containers() + if len(containers) != 1 { + t.Fatalf("expected 1 container; got %#v", containers) + } + image = containers[0].Image.String() + if image != newImage.String() { + t.Errorf("expected container image %q, got %q", newImage.String(), image) + } + if containers[0].Name != expectedContainer { + t.Errorf("expected container name %q, got %q", expectedContainer, containers[0].Name) + } +} + +func TestParseImageObjectFormat(t *testing.T) { + expectedImageName := "bitnami/mariadb" + expectedImageTag := "10.1.30-r1" + expectedImage := expectedImageName + ":" + expectedImageTag + + doc := `--- +apiVersion: helm.integrations.flux.weave.works/v1alpha2 +kind: FluxHelmRelease +metadata: + name: mariadb + namespace: maria + labels: + chart: mariadb +spec: + chartGitPath: mariadb + values: + first: post + image: + repository: ` + expectedImageName + ` + tag: ` + expectedImageTag + ` + persistence: + enabled: false +` + + resources, err := ParseMultidoc([]byte(doc), "test") + if err != nil { + t.Fatal(err) + } + res, ok := resources["maria:fluxhelmrelease/mariadb"] + if !ok { + t.Fatalf("expected resource not found; instead got %#v", resources) + } + fhr, ok := res.(resource.Workload) + if !ok { + t.Fatalf("expected resource to be a Workload, instead got %#v", res) + } + + containers := fhr.Containers() + if len(containers) != 1 { + t.Errorf("expected 1 container; got %#v", containers) + } + image := containers[0].Image.String() + if image != expectedImage { + t.Errorf("expected container image %q, got %q", expectedImage, image) + } +} + +func TestParseNamedImageObjectFormat(t *testing.T) { + expectedContainer := "db" + expectedImageName := "bitnami/mariadb" + expectedImageTag := "10.1.30-r1" + expectedImage := expectedImageName + ":" + expectedImageTag + + doc := `--- +apiVersion: helm.integrations.flux.weave.works/v1alpha2 +kind: FluxHelmRelease +metadata: + name: mariadb + namespace: maria + labels: + chart: mariadb +spec: + chartGitPath: mariadb + values: + other: + not: "containing image" + ` + expectedContainer + `: + first: post + image: + repository: ` + expectedImageName + ` + tag: ` + expectedImageTag + ` + persistence: + enabled: false +` + + resources, err := ParseMultidoc([]byte(doc), "test") + if err != nil { + t.Fatal(err) + } + res, ok := resources["maria:fluxhelmrelease/mariadb"] + if !ok { + t.Fatalf("expected resource not found; instead got %#v", resources) + } + fhr, ok := res.(resource.Workload) + if !ok { + t.Fatalf("expected resource to be a Workload, instead got %#v", res) + } + + containers := fhr.Containers() + if len(containers) != 1 { + t.Fatalf("expected 1 container; got %#v", containers) + } + image := containers[0].Image.String() + if image != expectedImage { + t.Errorf("expected container image %q, got %q", expectedImage, image) + } + if containers[0].Name != expectedContainer { + t.Errorf("expected container name %q, got %q", expectedContainer, containers[0].Name) + } + + newImage := containers[0].Image.WithNewTag("some-other-tag") + if err := fhr.SetContainerImage(expectedContainer, newImage); err != nil { + t.Error(err) + } + + containers = fhr.Containers() + if len(containers) != 1 { + t.Fatalf("expected 1 container; got %#v", containers) + } + image = containers[0].Image.String() + if image != newImage.String() { + t.Errorf("expected container image %q, got %q", newImage.String(), image) + } + if containers[0].Name != expectedContainer { + t.Errorf("expected container name %q, got %q", expectedContainer, containers[0].Name) + } +} + +func TestParseAllFormatsInOne(t *testing.T) { + + type container struct { + name, image, tag string + } + + // *NB* the containers will be calculated based on the order + // 1. the entry for 'image' if present + // 2. the order of the keys in `values`. + // + // To avoid having to mess around later, I have cooked the order + // of these so they can be compared directly to the return value. + expected := []container{ + {ReleaseContainerName, "repo/imageOne", "tagOne"}, + {"AAA", "repo/imageTwo", "tagTwo"}, + {"ZZZ", "repo/imageThree", "tagThree"}, + } + + doc := `--- +apiVersion: helm.integrations.flux.weave.works/v1alpha2 +kind: FluxHelmRelease +metadata: + name: test + namespace: test +spec: + chartGitPath: test + values: + # top-level image + image: ` + expected[0].image + ":" + expected[0].tag + ` + + # under .container, as image and tag entries + ` + expected[1].name + `: + image: ` + expected[1].image + ` + tag: ` + expected[1].tag + ` + + # under .container.image, as repository and tag entries + ` + expected[2].name + `: + image: + repository: ` + expected[2].image + ` + tag: ` + expected[2].tag + ` + persistence: + enabled: false +` + + resources, err := ParseMultidoc([]byte(doc), "test") + if err != nil { + t.Fatal(err) + } + res, ok := resources["test:fluxhelmrelease/test"] + if !ok { + t.Fatalf("expected resource not found; instead got %#v", resources) + } + fhr, ok := res.(resource.Workload) + if !ok { + t.Fatalf("expected resource to be a Workload, instead got %#v", res) + } + + containers := fhr.Containers() + if len(containers) != len(expected) { + t.Fatalf("expected %d containers, got %d", len(expected), len(containers)) + } + for i, c0 := range expected { + c1 := containers[i] + if c1.Name != c0.name { + t.Errorf("names do not match %q != %q", c0, c1) + } + c0image := fmt.Sprintf("%s:%s", c0.image, c0.tag) + if c1.Image.String() != c0image { + t.Errorf("images do not match %q != %q", c0image, c1.Image.String()) + } + } +} diff --git a/cluster/kubernetes/resource/spec.go b/cluster/kubernetes/resource/spec.go index 2e8b4f55f..1a9d546b5 100644 --- a/cluster/kubernetes/resource/spec.go +++ b/cluster/kubernetes/resource/spec.go @@ -22,8 +22,12 @@ type PodTemplate struct { func (t PodTemplate) Containers() []resource.Container { var result []resource.Container + // FIXME(https://github.com/weaveworks/flux/issues/1269): account for possible errors (x2) for _, c := range t.Spec.Containers { - // FIXME(michael): account for possible errors here + im, _ := image.ParseRef(c.Image) + result = append(result, resource.Container{Name: c.Name, Image: im}) + } + for _, c := range t.Spec.InitContainers { im, _ := image.ParseRef(c.Image) result = append(result, resource.Container{Name: c.Name, Image: im}) } @@ -37,6 +41,12 @@ func (t PodTemplate) SetContainerImage(container string, ref image.Ref) error { return nil } } + for i, c := range t.Spec.InitContainers { + if c.Name == container { + t.Spec.Containers[i].Image = ref.String() + return nil + } + } return fmt.Errorf("container %q not found in workload", container) } @@ -44,6 +54,7 @@ type PodSpec struct { ImagePullSecrets []struct{ Name string } Volumes []Volume Containers []ContainerSpec + InitContainers []ContainerSpec `yaml:"initContainers"` } type Volume struct { diff --git a/cluster/kubernetes/resourcekinds.go b/cluster/kubernetes/resourcekinds.go index e447ca80c..11c18a7b3 100644 --- a/cluster/kubernetes/resourcekinds.go +++ b/cluster/kubernetes/resourcekinds.go @@ -66,6 +66,15 @@ func (pc podController) toClusterController(resourceID flux.ResourceID) cluster. } clusterContainers = append(clusterContainers, resource.Container{Name: container.Name, Image: ref}) } + for _, container := range pc.podTemplate.Spec.InitContainers { + ref, err := image.ParseRef(container.Image) + if err != nil { + clusterContainers = nil + excuse = err.Error() + break + } + clusterContainers = append(clusterContainers, resource.Container{Name: container.Name, Image: ref}) + } var antecedent flux.ResourceID if ante, ok := pc.GetAnnotations()[AntecedentAnnotation]; ok { diff --git a/docker/Dockerfile.flux b/docker/Dockerfile.flux index 33ba08dfe..3f42a7147 100644 --- a/docker/Dockerfile.flux +++ b/docker/Dockerfile.flux @@ -29,7 +29,7 @@ LABEL maintainer="Weaveworks " \ ENTRYPOINT [ "/sbin/tini", "--", "fluxd" ] # Get the kubeyaml binary (files) and put them on the path -COPY --from=quay.io/squaremo/kubeyaml:0.3.3 /usr/lib/kubeyaml /usr/lib/kubeyaml/ +COPY --from=quay.io/squaremo/kubeyaml:0.4.2 /usr/lib/kubeyaml /usr/lib/kubeyaml/ ENV PATH=/bin:/usr/bin:/usr/local/bin:/usr/lib/kubeyaml COPY ./kubeconfig /root/.kube/config