diff options
| author | noah <noah@hackedu.io> | 2021-05-11 20:17:38 +1200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-05-11 10:17:38 +0200 |
| commit | add387ecfa5c59b34119eabd8b81c1daddcdc06e (patch) | |
| tree | baa5d5c63641505ac16aa849316b85e1bf741190 | |
| parent | b47f7e961649acf4c9e7c98d575284a143707ce8 (diff) | |
feat: allow write-back to actual kustomization files (#200)
* feat: allow write-back to actual kustomization files #199
* fix: was not handling default path correctly
default is the source path
* fix: sort imports
| -rw-r--r-- | .github/actions/spelling/allow.txt | 2 | ||||
| -rw-r--r-- | Dockerfile | 3 | ||||
| -rw-r--r-- | docs/configuration/applications.md | 29 | ||||
| -rw-r--r-- | go.mod | 12 | ||||
| -rw-r--r-- | go.sum | 28 | ||||
| -rw-r--r-- | pkg/argocd/git.go | 171 | ||||
| -rw-r--r-- | pkg/argocd/git_test.go | 49 | ||||
| -rw-r--r-- | pkg/argocd/update.go | 45 | ||||
| -rw-r--r-- | pkg/argocd/update_test.go | 167 | ||||
| -rw-r--r-- | pkg/common/constants.go | 2 | ||||
| -rw-r--r-- | pkg/image/image_test.go | 9 |
11 files changed, 435 insertions, 82 deletions
diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 72fa87a..787ae03 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -107,6 +107,7 @@ kubectl kubefake kubernetes Kustomization +kustomization kustomize ldflags LDFLAGS @@ -232,6 +233,7 @@ workflow workflows www yaml +yml yourimage yourorg yourtoken @@ -2,6 +2,9 @@ FROM golang:1.14.13 AS builder RUN mkdir -p /src/argocd-image-updater WORKDIR /src/argocd-image-updater +# cache dependencies as a layer for faster rebuilds +COPY go.mod go.sum ./ +RUN go mod download COPY . . RUN mkdir -p dist && \ diff --git a/docs/configuration/applications.md b/docs/configuration/applications.md index a53cfb8..00cd442 100644 --- a/docs/configuration/applications.md +++ b/docs/configuration/applications.md @@ -257,3 +257,32 @@ In order to test a template before configuring it for use in Image Updater, you can store the template you want to use in a temporary file, and then use the `argocd-image-updater template /path/to/file` command to render the template using pre-defined data and see its outcome on the terminal. + +#### Git Write-Back Target + +By default, git write-back will create or update `.argocd-source-<appName>.yaml`. + +If you are using Kustomize and want the image updates available for normal use with `kustomize`, +you may set the `write-back-target` to `kustomization`. This method commits changes to the Kustomization +file back to git as though you ran `kustomize edit set image`. + +```yaml +argocd-image-updater.argoproj.io/write-back-method: git # all git options are supported +argocd-image-updater.argoproj.io/write-back-target: kustomization +``` + +You may also specify which kustomization to update with either a path relative to the project source path... + +```yaml +argocd-image-updater.argoproj.io/write-back-target: "kustomization:../../base" +# if the Application spec.source.path = config/overlays/foo, this would update the kustomization in config/base +``` + +...or absolute with respect to the repository: + +```yaml +# absolute paths start with / +argocd-image-updater.argoproj.io/write-back-target: "kustomization:/config/overlays/bar" +``` + +Note that the Kustomization directory needs to be specified, not a file, like when using Kustomize. @@ -9,6 +9,8 @@ require ( github.com/argoproj/pkg v0.0.0-20200624215116-23e74cb168fe github.com/docker/distribution v2.7.1+incompatible github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect + github.com/go-openapi/spec v0.19.5 // indirect + github.com/google/go-cmp v0.4.0 // indirect github.com/gorilla/mux v1.7.4 // indirect github.com/nokia/docker-registry-client v0.0.0-20201015093031-af1a6d3b4fb1 github.com/patrickmn/go-cache v2.1.0+incompatible @@ -17,14 +19,18 @@ require ( github.com/spf13/cobra v1.0.0 github.com/stretchr/testify v1.6.1 go.uber.org/ratelimit v0.1.1-0.20201110185707-e86515f0dda9 - golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 - golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/src-d/go-git.v4 v4.13.1 - gopkg.in/yaml.v2 v2.3.0 + gopkg.in/yaml.v2 v2.4.0 k8s.io/api v1.18.8 k8s.io/apimachinery v1.18.8 k8s.io/client-go v11.0.1-0.20190816222228-6d55c1b1f1ca+incompatible k8s.io/kubectl v1.18.8 // indirect + sigs.k8s.io/kustomize v2.0.3+incompatible ) replace ( @@ -219,8 +219,9 @@ github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nA github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= -github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.5 h1:Xm0Ao53uqnk9QE/LlYV5DEU09UAgpliA85QoT9LzqPw= +github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= @@ -303,8 +304,9 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/cadvisor v0.35.0/go.mod h1:1nql6U13uTHaLYB8rLS5x9IJc2qT6Xd/Tr1sTX6NE48= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-jsonnet v0.16.0/go.mod h1:sOcuej3UW1vpPTZOr8L7RQimqai1a57bt5j22LzGZCw= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -658,8 +660,9 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -698,8 +701,9 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -712,8 +716,9 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -740,14 +745,17 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= @@ -782,6 +790,9 @@ golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDq golang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= gonum.org/v1/gonum v0.6.2/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= @@ -837,8 +848,9 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/pkg/argocd/git.go b/pkg/argocd/git.go index dd80b00..1a70978 100644 --- a/pkg/argocd/git.go +++ b/pkg/argocd/git.go @@ -6,8 +6,15 @@ import ( "io/ioutil" "os" "path" + "path/filepath" "text/template" + "sigs.k8s.io/kustomize/pkg/commands/kustfile" + "sigs.k8s.io/kustomize/pkg/fs" + image2 "sigs.k8s.io/kustomize/pkg/image" + + "github.com/argoproj-labs/argocd-image-updater/pkg/image" + "github.com/argoproj-labs/argocd-image-updater/ext/git" "github.com/argoproj-labs/argocd-image-updater/pkg/log" @@ -51,25 +58,27 @@ func TemplateCommitMessage(tpl *template.Template, appName string, changeList [] return cmBuf.String() } +type changeWriter func(app *v1alpha1.Application, wbc *WriteBackConfig, gitC git.Client) (err error, skip bool) + // commitChanges commits any changes required for updating one or more images // after the UpdateApplication cycle has finished. -func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig) error { +func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, write changeWriter) error { creds, err := wbc.GetCreds(app) if err != nil { return fmt.Errorf("could not get creds for repo '%s': %v", app.Spec.Source.RepoURL, err) } - tempRoot, err := ioutil.TempDir(os.TempDir(), fmt.Sprintf("git-%s", app.Name)) - if err != nil { - return err - } - defer func() { - err := os.RemoveAll(tempRoot) - if err != nil { - log.Errorf("could not remove temp dir: %v", err) - } - }() var gitC git.Client if wbc.GitClient == nil { + tempRoot, err := ioutil.TempDir(os.TempDir(), fmt.Sprintf("git-%s", app.Name)) + if err != nil { + return err + } + defer func() { + err := os.RemoveAll(tempRoot) + if err != nil { + log.Errorf("could not remove temp dir: %v", err) + } + }() gitC, err = git.NewClientExt(app.Spec.Source.RepoURL, tempRoot, creds, false, false) if err != nil { return err @@ -115,12 +124,49 @@ func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig) error { if err != nil { return err } + + if err, skip := write(app, wbc, gitC); err != nil { + return err + } else if skip { + return nil + } + + commitOpts := &git.CommitOptions{} + if wbc.GitCommitMessage != "" { + cm, err := ioutil.TempFile("", "image-updater-commit-msg") + if err != nil { + return fmt.Errorf("cold not create temp file: %v", err) + } + log.Debugf("Writing commit message to %s", cm.Name()) + err = ioutil.WriteFile(cm.Name(), []byte(wbc.GitCommitMessage), 0600) + if err != nil { + _ = cm.Close() + return fmt.Errorf("could not write commit message to %s: %v", cm.Name(), err) + } + commitOpts.CommitMessagePath = cm.Name() + _ = cm.Close() + defer os.Remove(cm.Name()) + } + + err = gitC.Commit("", commitOpts) + if err != nil { + return err + } + err = gitC.Push("origin", checkOutBranch, false) + if err != nil { + return err + } + + return nil +} + +func writeOverrides(app *v1alpha1.Application, _ *WriteBackConfig, gitC git.Client) (err error, skip bool) { targetExists := true - targetFile := path.Join(tempRoot, app.Spec.Source.Path, fmt.Sprintf(".argocd-source-%s.yaml", app.Name)) + targetFile := path.Join(gitC.Root(), app.Spec.Source.Path, fmt.Sprintf(".argocd-source-%s.yaml", app.Name)) _, err = os.Stat(targetFile) if err != nil { if !os.IsNotExist(err) { - return err + return } else { targetExists = false } @@ -128,7 +174,7 @@ func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig) error { override, err := marshalParamsOverride(app) if err != nil { - return fmt.Errorf("could not marshal parameters: %v", err) + return } // If the target file already exist in the repository, we will check whether @@ -137,51 +183,96 @@ func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig) error { if targetExists { data, err := ioutil.ReadFile(targetFile) if err != nil { - return err + return err, false } if string(data) == string(override) { log.Debugf("target parameter file and marshaled data are the same, skipping commit.") - return nil + return nil, true } } err = ioutil.WriteFile(targetFile, override, 0600) if err != nil { - return err + return } if !targetExists { err = gitC.Add(targetFile) - if err != nil { - return err - } } + return +} - commitOpts := &git.CommitOptions{} - if wbc.GitCommitMessage != "" { - cm, err := ioutil.TempFile("", "image-updater-commit-msg") - if err != nil { - return fmt.Errorf("cold not create temp file: %v", err) - } - log.Debugf("Writing commit message to %s", cm.Name()) - err = ioutil.WriteFile(cm.Name(), []byte(wbc.GitCommitMessage), 0600) - if err != nil { - _ = cm.Close() - return fmt.Errorf("could not write commit message to %s: %v", cm.Name(), err) - } - commitOpts.CommitMessagePath = cm.Name() - _ = cm.Close() - defer os.Remove(cm.Name()) +var _ changeWriter = writeOverrides + +// writeKustomization writes any changes required for updating one or more images to a kustomization.yml +func writeKustomization(app *v1alpha1.Application, wbc *WriteBackConfig, gitC git.Client) (err error, skip bool) { + if oldDir, err := os.Getwd(); err != nil { + return err, false + } else { + defer func() { + _ = os.Chdir(oldDir) + }() } - err = gitC.Commit("", commitOpts) + base := filepath.Join(gitC.Root(), wbc.KustomizeBase) + if err := os.Chdir(base); err != nil { + return err, false + } + + log.Infof("updating base %s", base) + + kf, err := kustfile.NewKustomizationFile(fs.MakeRealFS()) if err != nil { - return err + return } - err = gitC.Push("origin", checkOutBranch, false) + kustomization, err := kf.Read() if err != nil { - return err + return } - return nil +Images: + for _, img := range app.Spec.Source.Kustomize.Images { + override := parseImageOverride(img) + for i, imgSet := range kustomization.Images { + if imgSet.Name == override.Name { + kustomization.Images[i] = override + continue Images + } + } + // wasn't an existing override, add one + kustomization.Images = append(kustomization.Images, override) + } + + if err := kf.Write(kustomization); err != nil { + return err, false + } + + return } + +func parseImageOverride(str v1alpha1.KustomizeImage) image2.Image { + // TODO is this a valid use? format could diverge + img := image.NewFromIdentifier(string(str)) + tagName := "" + tagDigest := "" + if img.ImageTag != nil { + tagName = img.ImageTag.TagName + tagDigest = img.ImageTag.TagDigest + } + if img.RegistryURL != "" { + // NewFromIdentifier strips off the registry + img.ImageName = img.RegistryURL + "/" + img.ImageName + } + if img.ImageAlias == "" { + img.ImageAlias = img.ImageName + img.ImageName = "" // inside baseball (see return): name isn't changing, just tag, so don't write newName + } + return image2.Image{ + Name: img.ImageAlias, + NewName: img.ImageName, + NewTag: tagName, + Digest: tagDigest, + } +} + +var _ changeWriter = writeKustomization diff --git a/pkg/argocd/git_test.go b/pkg/argocd/git_test.go index 94cabe0..503c8e5 100644 --- a/pkg/argocd/git_test.go +++ b/pkg/argocd/git_test.go @@ -9,7 +9,9 @@ import ( "github.com/argoproj-labs/argocd-image-updater/pkg/image" "github.com/argoproj-labs/argocd-image-updater/pkg/tag" + "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" "github.com/stretchr/testify/assert" + image2 "sigs.k8s.io/kustomize/pkg/image" ) func Test_TemplateCommitMessage(t *testing.T) { @@ -37,3 +39,50 @@ updates image bar/baz tag '2.0' to '2.1' assert.Equal(t, exp, r) }) } + +func Test_parseImageOverride(t *testing.T) { + cases := []struct { + name string + override v1alpha1.KustomizeImage + expected image2.Image + }{ + {"tag update", "ghcr.io:1234/foo/foo:123", image2.Image{ + Name: "ghcr.io:1234/foo/foo", + NewTag: "123", + }}, + {"image update", "ghcr.io:1234/foo/foo=ghcr.io:1234/bar", image2.Image{ + Name: "ghcr.io:1234/foo/foo", + NewName: "ghcr.io:1234/bar", + }}, + {"update everything", "ghcr.io:1234/foo/foo=1234.foo.com:9876/bar:123", image2.Image{ + Name: "ghcr.io:1234/foo/foo", + NewName: "1234.foo.com:9876/bar", + NewTag: "123", + }}, + {"change registry and tag", "ghcr.io:1234/foo/foo=1234.dkr.ecr.us-east-1.amazonaws.com/bar:123", image2.Image{ + Name: "ghcr.io:1234/foo/foo", + NewName: "1234.dkr.ecr.us-east-1.amazonaws.com/bar", + NewTag: "123", + }}, + {"change only registry", "0001.dkr.ecr.us-east-1.amazonaws.com/bar=1234.dkr.ecr.us-east-1.amazonaws.com/bar", image2.Image{ + Name: "0001.dkr.ecr.us-east-1.amazonaws.com/bar", + NewName: "1234.dkr.ecr.us-east-1.amazonaws.com/bar", + }}, + {"change image and set digest", "foo=acme/app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", image2.Image{ + Name: "foo", + NewName: "acme/app", + Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }}, + {"set digest", "acme/app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", image2.Image{ + Name: "acme/app", + Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }}, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, parseImageOverride(tt.override)) + }) + } + +} diff --git a/pkg/argocd/update.go b/pkg/argocd/update.go index 333d31a..e7d99c5 100644 --- a/pkg/argocd/update.go +++ b/pkg/argocd/update.go @@ -3,6 +3,7 @@ package argocd import ( "context" "fmt" + "path/filepath" "strings" "sync" "text/template" @@ -16,10 +17,9 @@ import ( "github.com/argoproj-labs/argocd-image-updater/pkg/registry" "github.com/argoproj-labs/argocd-image-updater/pkg/tag" - "gopkg.in/yaml.v2" - "github.com/argoproj/argo-cd/pkg/apiclient/application" "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" + "gopkg.in/yaml.v2" ) // Stores some statistics about the results of a run @@ -64,6 +64,7 @@ type WriteBackConfig struct { GitCommitUser string GitCommitEmail string GitCommitMessage string + KustomizeBase string } // The following are helper structs to only marshal the fields we require @@ -420,15 +421,12 @@ func getWriteBackConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesCl switch strings.TrimSpace(method) { case "git": wbc.Method = WriteBackGit - branch, ok := app.Annotations[common.GitBranchAnnotation] - if ok { - wbc.GitBranch = strings.TrimSpace(branch) + if target, ok := app.Annotations[common.WriteBackTargetAnnotation]; ok && strings.HasPrefix(target, common.KustomizationPrefix) { + wbc.KustomizeBase = parseTarget(target, app.Spec.Source.Path) } - credsSource, err := getGitCredsSource(creds, kubeClient) - if err != nil { - return nil, fmt.Errorf("invalid git credentials source: %v", err) + if err := parseGitConfig(app, kubeClient, wbc, creds); err != nil { + return nil, err } - wbc.GetCreds = credsSource default: return nil, fmt.Errorf("invalid update mechanism: %s", method) } @@ -436,6 +434,29 @@ func getWriteBackConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesCl return wbc, nil } +func parseTarget(target string, sourcePath string) (kustomizeBase string) { + if target == common.KustomizationPrefix { + return filepath.Join(sourcePath, ".") + } else if base := target[len(common.KustomizationPrefix)+1:]; strings.HasPrefix(base, "/") { + return base[1:] + } else { + return filepath.Join(sourcePath, base) + } +} + +func parseGitConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesClient, wbc *WriteBackConfig, creds string) error { + branch, ok := app.Annotations[common.GitBranchAnnotation] + if ok { + wbc.GitBranch = strings.TrimSpace(branch) + } + credsSource, err := getGitCredsSource(creds, kubeClient) + if err != nil { + return fmt.Errorf("invalid git credentials source: %v", err) + } + wbc.GetCreds = credsSource + return nil +} + func commitChangesLocked(app *v1alpha1.Application, wbc *WriteBackConfig, state *SyncIterationState) error { if wbc.RequiresLocking() { lock := state.GetRepositoryLock(app.Spec.Source.RepoURL) @@ -459,7 +480,11 @@ func commitChanges(app *v1alpha1.Application, wbc *WriteBackConfig) error { return err } case WriteBackGit: - return commitChangesGit(app, wbc) + // if the kustomize base is set, the target is a kustomization + if wbc.KustomizeBase != "" { + return commitChangesGit(app, wbc, writeKustomization) + } + return commitChangesGit(app, wbc, writeOverrides) default: return fmt.Errorf("unknown write back method set: %d", wbc.Method) } diff --git a/pkg/argocd/update_test.go b/pkg/argocd/update_test.go index b53387a..9f4aec9 100644 --- a/pkg/argocd/update_test.go +++ b/pkg/argocd/update_test.go @@ -3,6 +3,9 @@ package argocd import ( "errors" "fmt" + "io/ioutil" + "os" + "path/filepath" "strings" "testing" @@ -1032,6 +1035,42 @@ func Test_GetWriteBackConfig(t *testing.T) { assert.Equal(t, wbc.Method, WriteBackApplication) }) + t.Run("kustomization write-back config", func(t *testing.T) { + app := v1alpha1.Application{ + ObjectMeta: v1.ObjectMeta{ + Name: "testapp", + Annotations: map[string]string{ + "argocd-image-updater.argoproj.io/image-list": "nginx", + "argocd-image-updater.argoproj.io/write-back-method": "git", + "argocd-image-updater.argoproj.io/write-back-target": "kustomization:../bar", + }, + }, + Spec: v1alpha1.ApplicationSpec{ + Source: v1alpha1.ApplicationSource{ + RepoURL: "https://example.com/example", + TargetRevision: "main", + Path: "config/foo", + }, + }, + Status: v1alpha1.ApplicationStatus{ + SourceType: v1alpha1.ApplicationSourceTypeKustomize, + }, + } + + argoClient := argomock.ArgoCD{} + argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) + + kubeClient := kube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + } + + wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) + require.NoError(t, err) + require.NotNil(t, wbc) + assert.Equal(t, wbc.Method, WriteBackGit) + assert.Equal(t, wbc.KustomizeBase, "config/bar") + }) + t.Run("Default write-back config - argocd", func(t *testing.T) { app := v1alpha1.Application{ ObjectMeta: v1.ObjectMeta{ @@ -1366,9 +1405,8 @@ func Test_CommitUpdates(t *testing.T) { } t.Run("Good commit to target revision", func(t *testing.T) { - gitMock := &gitmock.Client{} - gitMock.On("Init").Return(nil) - gitMock.On("Fetch").Return(nil) + gitMock, _, cleanup := mockGit(t) + defer cleanup() gitMock.On("Checkout", mock.Anything).Run(func(args mock.Arguments) { args.Assert(t, "main") }).Return(nil) @@ -1385,9 +1423,8 @@ func Test_CommitUpdates(t *testing.T) { }) t.Run("Good commit to configured branch", func(t *testing.T) { - gitMock := &gitmock.Client{} - gitMock.On("Init").Return(nil) - gitMock.On("Fetch").Return(nil) + gitMock, _, cleanup := mockGit(t) + defer cleanup() gitMock.On("Checkout", mock.Anything).Run(func(args mock.Arguments) { args.Assert(t, "mybranch") }).Return(nil) @@ -1407,9 +1444,8 @@ func Test_CommitUpdates(t *testing.T) { t.Run("Good commit to default branch", func(t *testing.T) { app := app.DeepCopy() - gitMock := &gitmock.Client{} - gitMock.On("Init").Return(nil) - gitMock.On("Fetch").Return(nil) + gitMock, _, cleanup := mockGit(t) + defer cleanup() gitMock.On("Checkout", mock.Anything).Run(func(args mock.Arguments) { args.Assert(t, "mydefaultbranch") }).Return(nil) @@ -1427,11 +1463,67 @@ func Test_CommitUpdates(t *testing.T) { assert.NoError(t, err) }) + t.Run("Good commit to kustomization", func(t *testing.T) { + app := app.DeepCopy() + app.Annotations[common.WriteBackTargetAnnotation] = "kustomization" + app.Spec.Source.Kustomize = &v1alpha1.ApplicationSourceKustomize{Images: v1alpha1.KustomizeImages{"foo=bar", "bar=baz:123"}} + gitMock, dir, cleanup := mockGit(t) + defer cleanup() + kf := filepath.Join(dir, "kustomization.yml") + assert.NoError(t, ioutil.WriteFile(kf, []byte(` +kind: Kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +`), os.ModePerm)) + + gitMock.On("Checkout", mock.Anything).Run(func(args mock.Arguments) { + args.Assert(t, "mydefaultbranch") + }).Return(nil) + gitMock.On("Add", mock.Anything).Return(nil) + gitMock.On("Commit", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + gitMock.On("Push", mock.Anything, mock.Anything, mock.Anything).Return(nil) + gitMock.On("SymRefToBranch", mock.Anything).Return("mydefaultbranch", nil) + wbc, err := getWriteBackConfig(app, &kubeClient, &argoClient) + require.NoError(t, err) + wbc.GitClient = gitMock + app.Spec.Source.TargetRevision = "HEAD" + wbc.GitBranch = "" + + err = commitChanges(app, wbc) + assert.NoError(t, err) + kust, err := ioutil.ReadFile(kf) + assert.NoError(t, err) + assert.YAMLEq(t, ` +kind: Kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +images: + - name: foo + newName: bar + - name: bar + newName: baz + newTag: "123" +`, string(kust)) + + // test the merge case too + app.Spec.Source.Kustomize.Images = v1alpha1.KustomizeImages{"foo:123", "bar=qux"} + err = commitChanges(app, wbc) + assert.NoError(t, err) + kust, err = ioutil.ReadFile(kf) + assert.NoError(t, err) + assert.YAMLEq(t, ` +kind: Kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +images: + - name: foo + newTag: "123" + - name: bar + newName: qux +`, string(kust)) + }) + t.Run("Good commit with author information", func(t *testing.T) { app := app.DeepCopy() - gitMock := &gitmock.Client{} - gitMock.On("Init").Return(nil) - gitMock.On("Fetch").Return(nil) + gitMock, _, cleanup := mockGit(t) + defer cleanup() gitMock.On("Checkout", mock.Anything).Run(func(args mock.Arguments) { args.Assert(t, "mydefaultbranch") }).Return(nil) @@ -1526,9 +1618,8 @@ func Test_CommitUpdates(t *testing.T) { }) t.Run("Cannot commit", func(t *testing.T) { - gitMock := &gitmock.Client{} - gitMock.On("Init").Return(nil) - gitMock.On("Fetch").Return(nil) + gitMock, _, cleanup := mockGit(t) + defer cleanup() gitMock.On("Checkout", mock.Anything).Return(nil) gitMock.On("Add", mock.Anything).Return(nil) gitMock.On("Commit", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("cannot commit")) @@ -1542,9 +1633,8 @@ func Test_CommitUpdates(t *testing.T) { }) t.Run("Cannot push", func(t *testing.T) { - gitMock := &gitmock.Client{} - gitMock.On("Init").Return(nil) - gitMock.On("Fetch").Return(nil) + gitMock, _, cleanup := mockGit(t) + defer cleanup() gitMock.On("Checkout", mock.Anything).Return(nil) gitMock.On("Add", mock.Anything).Return(nil) gitMock.On("Commit", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) @@ -1559,9 +1649,8 @@ func Test_CommitUpdates(t *testing.T) { t.Run("Cannot resolve default branch", func(t *testing.T) { app := app.DeepCopy() - gitMock := &gitmock.Client{} - gitMock.On("Init").Return(nil) - gitMock.On("Fetch").Return(nil) + gitMock, _, cleanup := mockGit(t) + defer cleanup() gitMock.On("Checkout", mock.Anything).Return(nil) gitMock.On("Add", mock.Anything).Return(nil) gitMock.On("Commit", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) @@ -1577,3 +1666,39 @@ func Test_CommitUpdates(t *testing.T) { assert.Errorf(t, err, "failed to resolve ref") }) } + +func Test_parseTarget(t *testing.T) { + cases := []struct { + name string + expected string + target string + path string + }{ + {"default", ".", "kustomization", ""}, + {"explicit default", ".", "kustomization:.", "."}, + {"default path, explicit target", ".", "kustomization:.", ""}, + {"default target with path", "foo/bar", "kustomization", "foo/bar"}, + {"default both", ".", "kustomization", ""}, + {"absolute path", "foo", "kustomization:/foo", "bar"}, + {"relative path", "bar/foo", "kustomization:foo", "bar"}, + {"sibling path", "bar/baz", "kustomization:../baz", "bar/foo"}, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, parseTarget(tt.target, tt.path)) + }) + } +} + +func mockGit(t *testing.T) (gitMock *gitmock.Client, dir string, cleanup func()) { + dir, err := ioutil.TempDir("", "wb-kust") + assert.NoError(t, err) + gitMock = &gitmock.Client{} + gitMock.On("Root").Return(dir) + gitMock.On("Init").Return(nil) + gitMock.On("Fetch").Return(nil) + return gitMock, dir, func() { + _ = os.RemoveAll(dir) + } +} diff --git a/pkg/common/constants.go b/pkg/common/constants.go index b42f1a9..6c7fec3 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -44,6 +44,8 @@ const ( const ( WriteBackMethodAnnotation = ImageUpdaterAnnotationPrefix + "/write-back-method" GitBranchAnnotation = ImageUpdaterAnnotationPrefix + "/git-branch" + WriteBackTargetAnnotation = ImageUpdaterAnnotationPrefix + "/write-back-target" + KustomizationPrefix = "kustomization" ) // The default Git commit message's template diff --git a/pkg/image/image_test.go b/pkg/image/image_test.go index 3e74aaa..9cde998 100644 --- a/pkg/image/image_test.go +++ b/pkg/image/image_test.go @@ -54,6 +54,15 @@ func Test_ParseImageTags(t *testing.T) { assert.Equal(t, "0.1", image.ImageTag.TagName) }) + t.Run("Parse valid image name with source name and registry info with port", func(t *testing.T) { + image := NewFromIdentifier("ghcr.io:4567/jannfis/orig-image=gcr.io:1234/jannfis/test-image:0.1") + assert.Equal(t, "gcr.io:1234", image.RegistryURL) + assert.Equal(t, "ghcr.io:4567/jannfis/orig-image", image.ImageAlias) + assert.Equal(t, "jannfis/test-image", image.ImageName) + require.NotNil(t, image.ImageTag) + assert.Equal(t, "0.1", image.ImageTag.TagName) + }) + t.Run("Parse image without version source name and registry info", func(t *testing.T) { image := NewFromIdentifier("jannfis/orig-image=gcr.io/jannfis/test-image") assert.Equal(t, "gcr.io", image.RegistryURL) |
