diff --git a/Gopkg.lock b/Gopkg.lock index 52c32605924bf..81581ed19bcc5 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -45,14 +45,14 @@ [[projects]] branch = "master" - digest = "1:f624a361a427f4c9853f1d4d0bd47dff8323cef0054708d8df56cefe03ce4ac5" + digest = "1:e8ec0abbf32fdcc9f7eb14c0656c1d0fc2fc7ec8f60dff4b7ac080c50afd8e49" name = "github.com/argoproj/pkg" packages = [ "exec", "time", ] pruneopts = "" - revision = "fc8a50f323ce16f93a34632f7abf780977386fe8" + revision = "88ab0e836a8e8c70bc297c5764669bd7da27afd1" [[projects]] digest = "1:d8a2bb36a048d1571bcc1aee208b61f39dc16c6c53823feffd37449dde162507" diff --git a/reposerver/repository/repository.go b/reposerver/repository/repository.go index a675f7f9d99fd..42fc812dc92c1 100644 --- a/reposerver/repository/repository.go +++ b/reposerver/repository/repository.go @@ -206,7 +206,7 @@ func generateManifests(appPath string, q *ManifestRequest) (*ManifestResponse, e } case AppSourceKustomize: k := kustomize.NewKustomizeApp(appPath) - targetObjs, err = k.Build() + targetObjs, params, err = k.Build(q.Namespace, q.ComponentParameterOverrides) case AppSourceDirectory: targetObjs, err = findManifests(appPath) } diff --git a/util/kustomize/kustomize.go b/util/kustomize/kustomize.go index 23338e5813508..bd9af9ebfaa50 100644 --- a/util/kustomize/kustomize.go +++ b/util/kustomize/kustomize.go @@ -1,16 +1,20 @@ package kustomize import ( - "github.com/argoproj/pkg/exec" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "fmt" + "os/exec" + "strings" + "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/util/kube" + argoexec "github.com/argoproj/pkg/exec" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) // Kustomize provides wrapper functionality around the `kustomize` command. type Kustomize interface { - // Build returns a list of unstructured objects from a `kustomize build` command - Build() ([]*unstructured.Unstructured, error) + // Build returns a list of unstructured objects from a `kustomize build` command and extract supported parameters + Build(namespace string, overrides []*v1alpha1.ComponentParameter) ([]*unstructured.Unstructured, []*v1alpha1.ComponentParameter, error) } // NewKustomizeApp create a new wrapper to run commands on the `kustomize` command-line tool. @@ -22,10 +26,83 @@ type kustomize struct { path string } -func (k *kustomize) Build() ([]*unstructured.Unstructured, error) { - out, err := exec.RunCommand("kustomize", "build", k.path) +func (k *kustomize) Build(namespace string, overrides []*v1alpha1.ComponentParameter) ([]*unstructured.Unstructured, []*v1alpha1.ComponentParameter, error) { + if namespace != "" { + cmd := exec.Command("kustomize", "edit", "set", "namespace", namespace) + cmd.Dir = k.path + _, err := argoexec.RunCommandExt(cmd) + if err != nil { + return nil, nil, err + } + } + + for _, override := range overrides { + cmd := exec.Command("kustomize", "edit", "set", "imagetag", fmt.Sprintf("%s:%s", override.Name, override.Value)) + cmd.Dir = k.path + _, err := argoexec.RunCommandExt(cmd) + if err != nil { + return nil, nil, err + } + } + + out, err := argoexec.RunCommand("kustomize", "build", k.path) + if err != nil { + return nil, nil, err + } + + objs, err := kube.SplitYAML(out) if err != nil { - return nil, err + return nil, nil, err + } + + return objs, append(getImageParameters(objs)), nil +} + +func getImageParameters(objs []*unstructured.Unstructured) []*v1alpha1.ComponentParameter { + images := make(map[string]string) + for _, obj := range objs { + for _, img := range getImages(obj.Object) { + parts := strings.Split(img, ":") + if len(parts) > 1 { + images[parts[0]] = parts[1] + } else { + images[img] = "latest" + } + } + } + var params []*v1alpha1.ComponentParameter + for img, version := range images { + params = append(params, &v1alpha1.ComponentParameter{ + Component: "imagetag", + Name: img, + Value: version, + }) + } + return params +} + +func getImages(object map[string]interface{}) []string { + var images []string + for k, v := range object { + if array, ok := v.([]interface{}); ok { + if k == "containers" || k == "initContainers" { + for _, obj := range array { + if mapObj, isMapObj := obj.(map[string]interface{}); isMapObj { + if image, hasImage := mapObj["image"]; hasImage { + images = append(images, fmt.Sprintf("%s", image)) + } + } + } + } else { + for i := range array { + if mapObj, isMapObj := array[i].(map[string]interface{}); isMapObj { + images = append(images, getImages(mapObj)...) + } + } + } + } else if objMap, ok := v.(map[string]interface{}); ok { + images = append(images, getImages(objMap)...) + } } - return kube.SplitYAML(out) + return images } diff --git a/util/kustomize/kustomize_test.go b/util/kustomize/kustomize_test.go new file mode 100644 index 0000000000000..b1ba6e7887218 --- /dev/null +++ b/util/kustomize/kustomize_test.go @@ -0,0 +1,60 @@ +package kustomize + +import ( + "io/ioutil" + "path" + "testing" + + "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" + + "github.com/argoproj/pkg/exec" + "github.com/stretchr/testify/assert" +) + +func testDataDir() (string, error) { + res, err := ioutil.TempDir("", "kustomize-test") + if err != nil { + return "", err + } + _, err = exec.RunCommand("cp", "-r", "./testdata", res) + if err != nil { + return "", err + } + return path.Join(res, "testdata"), nil +} + +func TestKustomizeBuild(t *testing.T) { + appPath, err := testDataDir() + assert.Nil(t, err) + + kustomize := NewKustomizeApp(appPath) + objs, params, err := kustomize.Build("mynamespace", []*v1alpha1.ComponentParameter{{ + Component: "imagetag", + Name: "k8s.gcr.io/nginx-slim", + Value: "latest", + }}) + assert.Nil(t, err) + if err != nil { + assert.Equal(t, len(objs), 2) + assert.Equal(t, len(params), 2) + } + for _, obj := range objs { + switch obj.GetKind() { + case "StatefulSet": + assert.Equal(t, "web", obj.GetName()) + case "Deployment": + assert.Equal(t, "nginx-deployment", obj.GetName()) + } + assert.Equal(t, "mynamespace", obj.GetNamespace()) + } + + for _, param := range params { + switch param.Value { + case "nginx": + assert.Equal(t, "1.15.4", param.Value) + case "k8s.gcr.io/nginx-slim": + assert.Equal(t, "latest", param.Value) + } + assert.Equal(t, "imagetag", param.Component) + } +} diff --git a/util/kustomize/testdata/deployment.yaml b/util/kustomize/testdata/deployment.yaml new file mode 100644 index 0000000000000..545961bb6094d --- /dev/null +++ b/util/kustomize/testdata/deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.15.4 + ports: + - containerPort: 80 \ No newline at end of file diff --git a/util/kustomize/testdata/kustomization.yaml b/util/kustomize/testdata/kustomization.yaml new file mode 100644 index 0000000000000..24d406dcb1db0 --- /dev/null +++ b/util/kustomize/testdata/kustomization.yaml @@ -0,0 +1,3 @@ +resources: + - ./deployment.yaml + - ./statefullset.yaml diff --git a/util/kustomize/testdata/statefullset.yaml b/util/kustomize/testdata/statefullset.yaml new file mode 100644 index 0000000000000..071a7f9f43ab0 --- /dev/null +++ b/util/kustomize/testdata/statefullset.yaml @@ -0,0 +1,34 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: web +spec: + selector: + matchLabels: + app: nginx # has to match .spec.template.metadata.labels + serviceName: "nginx" + replicas: 3 # by default is 1 + template: + metadata: + labels: + app: nginx # has to match .spec.selector.matchLabels + spec: + terminationGracePeriodSeconds: 10 + containers: + - name: nginx + image: k8s.gcr.io/nginx-slim:0.8 + ports: + - containerPort: 80 + name: web + volumeMounts: + - name: www + mountPath: /usr/share/nginx/html + volumeClaimTemplates: + - metadata: + name: www + spec: + accessModes: [ "ReadWriteOnce" ] + storageClassName: "my-storage-class" + resources: + requests: + storage: 1Gi \ No newline at end of file