From 89ddd0dffbc91086db7295f1c8806e3508c6a905 Mon Sep 17 00:00:00 2001 From: Alexander Matyushentsev Date: Wed, 17 Mar 2021 21:35:18 -0700 Subject: [PATCH] feat: support 'Replace=true' sync option (#246) Signed-off-by: Alexander Matyushentsev --- go.sum | 16 ---- pkg/sync/common/types.go | 2 + pkg/sync/sync_context.go | 35 ++++++-- pkg/sync/sync_context_test.go | 38 ++++++++ pkg/utils/kube/ctl.go | 153 ++++++++++++++++++++++++++++---- pkg/utils/kube/kubetest/mock.go | 63 ++++++++++--- 6 files changed, 256 insertions(+), 51 deletions(-) diff --git a/go.sum b/go.sum index 1835c28b1..071dfd29e 100644 --- a/go.sum +++ b/go.sum @@ -183,7 +183,6 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.3.0 h1:q4c+kbcR0d5rSurhBR8dIgieOaYpXtsdTYfx22Cu6rs= github.com/go-logr/logr v0.3.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= @@ -242,13 +241,11 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 h1:5ZkaAPbicIKTF2I64qf5Fh8Aa83Q/dnOafMYV0OMwjA= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= @@ -266,7 +263,6 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= @@ -280,7 +276,6 @@ github.com/google/cadvisor v0.38.7/go.mod h1:1OFB9sOOMkBdUBGCO/1SArawTnDscgMzTod 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/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-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= @@ -296,7 +291,6 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -524,7 +518,6 @@ github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= @@ -594,7 +587,6 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -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/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -632,7 +624,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -671,7 +662,6 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY 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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -736,14 +726,12 @@ golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fq 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/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/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/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/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -788,11 +776,9 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200616133436-c1934b75d054 h1:HHeAlu5H9b71C+Fx0K+1dGgVFN1DM1/wz4aoGOA5qS8= golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 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= @@ -857,7 +843,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= @@ -925,7 +910,6 @@ k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8 k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/heapster v1.2.0-beta.1/go.mod h1:h1uhptVXMwC8xtZBYsPXKVi8fpdlYkTs6k949KozGrM= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= diff --git a/pkg/sync/common/types.go b/pkg/sync/common/types.go index 2ff9ef05e..e9613b7c8 100644 --- a/pkg/sync/common/types.go +++ b/pkg/sync/common/types.go @@ -25,6 +25,8 @@ const ( SyncOptionsDisableValidation = "Validate=false" // Sync option that enables pruneLast SyncOptionPruneLast = "PruneLast=true" + // Sync option that enables use of replace or create command instead of apply + SyncOptionReplace = "Replace=true" ) type PermissionValidator func(un *unstructured.Unstructured, res *metav1.APIResource) error diff --git a/pkg/sync/sync_context.go b/pkg/sync/sync_context.go index 6d55fce5c..a6bafcc98 100644 --- a/pkg/sync/sync_context.go +++ b/pkg/sync/sync_context.go @@ -172,6 +172,12 @@ func WithSyncWaveHook(syncWaveHook common.SyncWaveHook) SyncOpt { } } +func WithReplace(replace bool) SyncOpt { + return func(ctx *syncContext) { + ctx.replace = replace + } +} + // NewSyncContext creates new instance of a SyncContext func NewSyncContext( revision string, @@ -305,6 +311,7 @@ type syncContext struct { skipHooks bool resourcesFilter func(key kube.ResourceKey, target *unstructured.Unstructured, live *unstructured.Unstructured) bool prune bool + replace bool pruneLast bool prunePropagationPolicy *metav1.DeletionPropagation @@ -800,17 +807,35 @@ func (sc *syncContext) ensureCRDReady(name string) { }) } -func (sc *syncContext) applyObject(targetObj *unstructured.Unstructured, dryRun bool, force bool, validate bool) (common.ResultCode, string) { +func (sc *syncContext) applyObject(t *syncTask, dryRun bool, force bool, validate bool) (common.ResultCode, string) { dryRunStrategy := cmdutil.DryRunNone if dryRun { dryRunStrategy = cmdutil.DryRunClient } - message, err := sc.kubectl.ApplyResource(context.TODO(), sc.rawConfig, targetObj, targetObj.GetNamespace(), dryRunStrategy, force, validate) + + var err error + var message string + shouldReplace := sc.replace || resourceutil.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, common.SyncOptionReplace) + // always use 'kubectl apply' for CRDs since 'replace' might recreate resource and so delete all CRD instances + if shouldReplace && !kube.IsCRD(t.targetObj) { + if t.liveObj != nil { + message, err = sc.kubectl.ReplaceResource(context.TODO(), sc.rawConfig, t.targetObj, t.targetObj.GetNamespace(), dryRunStrategy, force) + } else { + _, err = sc.kubectl.CreateResource(context.TODO(), sc.rawConfig, t.targetObj, t.targetObj.GetNamespace(), dryRunStrategy) + if err == nil { + message = fmt.Sprintf("%s/%s created", t.targetObj.GetKind(), t.targetObj.GetName()) + } else { + message = fmt.Sprintf("error when creating: %v", err.Error()) + } + } + } else { + message, err = sc.kubectl.ApplyResource(context.TODO(), sc.rawConfig, t.targetObj, t.targetObj.GetNamespace(), dryRunStrategy, force, validate) + } if err != nil { return common.ResultCodeSyncFailed, err.Error() } - if kube.IsCRD(targetObj) && !dryRun { - sc.ensureCRDReady(targetObj.GetName()) + if kube.IsCRD(t.targetObj) && !dryRun { + sc.ensureCRDReady(t.targetObj.GetName()) } return common.ResultCodeSynced, message } @@ -1054,7 +1079,7 @@ func (sc *syncContext) processCreateTasks(state runState, tasks syncTasks, dryRu logCtx := sc.log.WithValues("dryRun", dryRun, "task", t) logCtx.V(1).Info("Applying") validate := sc.validate && !resourceutil.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, common.SyncOptionsDisableValidation) - result, message := sc.applyObject(t.targetObj, dryRun, sc.force, validate) + result, message := sc.applyObject(t, dryRun, sc.force, validate) if result == common.ResultCodeSyncFailed { logCtx.WithValues("message", message).Info("Apply failed") state = failed diff --git a/pkg/sync/sync_context_test.go b/pkg/sync/sync_context_test.go index d0b088530..fd05ab7ba 100644 --- a/pkg/sync/sync_context_test.go +++ b/pkg/sync/sync_context_test.go @@ -405,6 +405,44 @@ func TestSyncOptionValidate(t *testing.T) { } } +func withReplaceAnnotation(un *unstructured.Unstructured) *unstructured.Unstructured { + un.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: synccommon.SyncOptionReplace}) + return un +} + +func TestSync_Replace(t *testing.T) { + testCases := []struct { + name string + target *unstructured.Unstructured + live *unstructured.Unstructured + commandUsed string + }{ + {"NoAnnotation", NewPod(), NewPod(), "apply"}, + {"AnnotationIsSet", withReplaceAnnotation(NewPod()), NewPod(), "replace"}, + {"LiveObjectMissing", withReplaceAnnotation(NewPod()), nil, "create"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + syncCtx := newTestSyncCtx() + + tc.target.SetNamespace(FakeArgoCDNamespace) + if tc.live != nil { + tc.live.SetNamespace(FakeArgoCDNamespace) + } + syncCtx.resources = groupResources(ReconciliationResult{ + Live: []*unstructured.Unstructured{tc.live}, + Target: []*unstructured.Unstructured{tc.target}, + }) + + syncCtx.Sync() + + kubectl, _ := syncCtx.kubectl.(*kubetest.MockKubectlCmd) + assert.Equal(t, tc.commandUsed, kubectl.GetLastResourceCommand(kube.GetResourceKey(tc.target))) + }) + } +} + func TestSelectiveSyncOnly(t *testing.T) { pod1 := NewPod() pod1.SetName("pod-1") diff --git a/pkg/utils/kube/ctl.go b/pkg/utils/kube/ctl.go index fc5dd95a9..135937afa 100644 --- a/pkg/utils/kube/ctl.go +++ b/pkg/utils/kube/ctl.go @@ -13,6 +13,7 @@ import ( "golang.org/x/sync/errgroup" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -24,6 +25,7 @@ import ( "k8s.io/client-go/rest" "k8s.io/kubectl/pkg/cmd/apply" "k8s.io/kubectl/pkg/cmd/auth" + "k8s.io/kubectl/pkg/cmd/replace" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" @@ -38,6 +40,8 @@ type OnKubectlRunFunc func(command string) (CleanupFunc, error) type Kubectl interface { ApplyResource(ctx context.Context, config *rest.Config, obj *unstructured.Unstructured, namespace string, dryRunStrategy cmdutil.DryRunStrategy, force, validate bool) (string, error) + ReplaceResource(ctx context.Context, config *rest.Config, obj *unstructured.Unstructured, namespace string, dryRunStrategy cmdutil.DryRunStrategy, force bool) (string, error) + CreateResource(ctx context.Context, config *rest.Config, obj *unstructured.Unstructured, namespace string, dryRunStrategy cmdutil.DryRunStrategy) (*unstructured.Unstructured, error) ConvertToVersion(obj *unstructured.Unstructured, group, version string) (*unstructured.Unstructured, error) DeleteResource(ctx context.Context, config *rest.Config, gvk schema.GroupVersionKind, name string, namespace string, deleteOptions metav1.DeleteOptions) error GetResource(ctx context.Context, config *rest.Config, gvk schema.GroupVersionKind, name string, namespace string) (*unstructured.Unstructured, error) @@ -214,13 +218,9 @@ func (k *KubectlCmd) DeleteResource(ctx context.Context, config *rest.Config, gv return resourceIf.Delete(ctx, name, deleteOptions) } -// ApplyResource performs an apply of a unstructured resource -func (k *KubectlCmd) ApplyResource(ctx context.Context, config *rest.Config, obj *unstructured.Unstructured, namespace string, dryRunStrategy cmdutil.DryRunStrategy, force, validate bool) (string, error) { - span := k.Tracer.StartSpan("ApplyResource") - span.SetBaggageItem("kind", obj.GetKind()) - span.SetBaggageItem("name", obj.GetName()) - defer span.Finish() - k.Log.Info(fmt.Sprintf("Applying resource %s/%s in cluster: %s, namespace: %s", obj.GetKind(), obj.GetName(), config.Host, namespace)) +type commandExecutor func(config *rest.Config, f cmdutil.Factory, ioStreams genericclioptions.IOStreams, fileName string, namespace string, dryRunStrategy cmdutil.DryRunStrategy) error + +func (k *KubectlCmd) runResourceCommand(ctx context.Context, config *rest.Config, obj *unstructured.Unstructured, namespace string, dryRunStrategy cmdutil.DryRunStrategy, executor commandExecutor) (string, error) { f, err := ioutil.TempFile(io.TempDir, "") if err != nil { return "", fmt.Errorf("Failed to generate temp file for kubeconfig: %v", err) @@ -287,19 +287,9 @@ func (k *KubectlCmd) ApplyResource(ctx context.Context, config *rest.Config, obj // last-applied-configuration annotation in the object. } - cleanup, err := k.processKubectlRun("apply") - if err != nil { - return "", err - } - defer cleanup() - // Run kubectl apply fact, ioStreams := kubeCmdFactory(f.Name(), namespace) - applyOpts, err := newApplyOptions(config, fact, ioStreams, manifestFile.Name(), namespace, validate, force, dryRunStrategy) - if err != nil { - return "", err - } - err = applyOpts.Run() + err = executor(config, fact, ioStreams, manifestFile.Name(), namespace, dryRunStrategy) if err != nil { return "", errors.New(cleanKubectlOutput(err.Error())) } @@ -328,6 +318,78 @@ func kubeCmdFactory(kubeconfig, ns string) (cmdutil.Factory, genericclioptions.I return f, ioStreams } +func (k *KubectlCmd) ReplaceResource(ctx context.Context, config *rest.Config, obj *unstructured.Unstructured, namespace string, dryRunStrategy cmdutil.DryRunStrategy, force bool) (string, error) { + span := k.Tracer.StartSpan("ReplaceResource") + span.SetBaggageItem("kind", obj.GetKind()) + span.SetBaggageItem("name", obj.GetName()) + defer span.Finish() + k.Log.Info(fmt.Sprintf("Replacing resource %s/%s in cluster: %s, namespace: %s", obj.GetKind(), obj.GetName(), config.Host, namespace)) + return k.runResourceCommand(ctx, config, obj, namespace, dryRunStrategy, func(config *rest.Config, f cmdutil.Factory, ioStreams genericclioptions.IOStreams, fileName string, namespace string, dryRunStrategy cmdutil.DryRunStrategy) error { + cleanup, err := k.processKubectlRun("replace") + if err != nil { + return err + } + defer cleanup() + + replaceOptions, err := newReplaceOptions(config, f, ioStreams, fileName, namespace, force, dryRunStrategy) + if err != nil { + return err + } + return replaceOptions.Run(f) + }) +} + +func (k *KubectlCmd) CreateResource(ctx context.Context, config *rest.Config, obj *unstructured.Unstructured, namespace string, dryRunStrategy cmdutil.DryRunStrategy) (*unstructured.Unstructured, error) { + gvk := obj.GroupVersionKind() + span := k.Tracer.StartSpan("CreateResource") + span.SetBaggageItem("kind", gvk.Kind) + span.SetBaggageItem("name", obj.GetName()) + defer span.Finish() + dynamicIf, err := dynamic.NewForConfig(config) + if err != nil { + return nil, err + } + disco, err := discovery.NewDiscoveryClientForConfig(config) + if err != nil { + return nil, err + } + apiResource, err := ServerResourceForGroupVersionKind(disco, gvk) + if err != nil { + return nil, err + } + resource := gvk.GroupVersion().WithResource(apiResource.Name) + resourceIf := ToResourceInterface(dynamicIf, apiResource, resource, namespace) + + createOptions := metav1.CreateOptions{} + switch dryRunStrategy { + case cmdutil.DryRunClient, cmdutil.DryRunServer: + createOptions.DryRun = []string{metav1.DryRunAll} + } + return resourceIf.Create(ctx, obj, createOptions) +} + +// ApplyResource performs an apply of a unstructured resource +func (k *KubectlCmd) ApplyResource(ctx context.Context, config *rest.Config, obj *unstructured.Unstructured, namespace string, dryRunStrategy cmdutil.DryRunStrategy, force, validate bool) (string, error) { + span := k.Tracer.StartSpan("ApplyResource") + span.SetBaggageItem("kind", obj.GetKind()) + span.SetBaggageItem("name", obj.GetName()) + defer span.Finish() + k.Log.Info(fmt.Sprintf("Applying resource %s/%s in cluster: %s, namespace: %s", obj.GetKind(), obj.GetName(), config.Host, namespace)) + return k.runResourceCommand(ctx, config, obj, namespace, dryRunStrategy, func(config *rest.Config, f cmdutil.Factory, ioStreams genericclioptions.IOStreams, fileName string, namespace string, dryRunStrategy cmdutil.DryRunStrategy) error { + cleanup, err := k.processKubectlRun("apply") + if err != nil { + return err + } + defer cleanup() + + applyOpts, err := newApplyOptions(config, f, ioStreams, fileName, namespace, validate, force, dryRunStrategy) + if err != nil { + return err + } + return applyOpts.Run() + }) +} + func newApplyOptions(config *rest.Config, f cmdutil.Factory, ioStreams genericclioptions.IOStreams, fileName string, namespace string, validate bool, force bool, dryRunStrategy cmdutil.DryRunStrategy) (*apply.ApplyOptions, error) { o := apply.NewApplyOptions(ioStreams) dynamicClient, err := dynamic.NewForConfig(config) @@ -381,6 +443,61 @@ func newApplyOptions(config *rest.Config, f cmdutil.Factory, ioStreams genericcl return o, nil } +func newReplaceOptions(config *rest.Config, f cmdutil.Factory, ioStreams genericclioptions.IOStreams, fileName string, namespace string, force bool, dryRunStrategy cmdutil.DryRunStrategy) (*replace.ReplaceOptions, error) { + o := replace.NewReplaceOptions(ioStreams) + + recorder, err := o.RecordFlags.ToRecorder() + if err != nil { + return nil, err + } + o.Recorder = recorder + + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return nil, err + } + + o.DeleteOptions, err = o.DeleteFlags.ToOptions(dynamicClient, o.IOStreams) + if err != nil { + return nil, err + } + discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) + if err != nil { + return nil, err + } + o.DryRunVerifier = resource.NewDryRunVerifier(dynamicClient, discoveryClient) + o.Builder = func() *resource.Builder { + return f.NewBuilder() + } + + switch dryRunStrategy { + case cmdutil.DryRunClient: + err = o.PrintFlags.Complete("%s (dry run)") + if err != nil { + return nil, err + } + case cmdutil.DryRunServer: + err = o.PrintFlags.Complete("%s (server dry run)") + if err != nil { + return nil, err + } + } + o.DryRunStrategy = dryRunStrategy + + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return nil, err + } + o.PrintObj = func(obj runtime.Object) error { + return printer.PrintObj(obj, o.Out) + } + + o.DeleteOptions.FilenameOptions.Filenames = []string{fileName} + o.Namespace = namespace + o.DeleteOptions.ForceDeletion = force + return o, nil +} + func newReconcileOptions(f cmdutil.Factory, kubeClient *kubernetes.Clientset, fileName string, ioStreams genericclioptions.IOStreams, namespace string, dryRunStrategy cmdutil.DryRunStrategy) (*auth.ReconcileOptions, error) { o := auth.NewReconcileOptions(ioStreams) o.RBACClient = kubeClient.RbacV1() diff --git a/pkg/utils/kube/kubetest/mock.go b/pkg/utils/kube/kubetest/mock.go index 417d9d928..44db6ef26 100644 --- a/pkg/utils/kube/kubetest/mock.go +++ b/pkg/utils/kube/kubetest/mock.go @@ -22,26 +22,46 @@ type KubectlOutput struct { } type MockKubectlCmd struct { - APIResources []kube.APIResourceInfo - Commands map[string]KubectlOutput - Events chan watch.Event - lastValidate bool - Version string - DynamicClient dynamic.Interface - APIGroups []metav1.APIGroup - lastValidateLock sync.RWMutex + APIResources []kube.APIResourceInfo + Commands map[string]KubectlOutput + Events chan watch.Event + Version string + DynamicClient dynamic.Interface + APIGroups []metav1.APIGroup + + lastCommandPerResource map[kube.ResourceKey]string + lastValidate bool + recordLock sync.RWMutex +} + +func (k *MockKubectlCmd) GetLastResourceCommand(key kube.ResourceKey) string { + k.recordLock.Lock() + defer k.recordLock.Unlock() + if k.lastCommandPerResource == nil { + return "" + } + return k.lastCommandPerResource[key] +} + +func (k *MockKubectlCmd) SetLastResourceCommand(key kube.ResourceKey, cmd string) { + k.recordLock.Lock() + if k.lastCommandPerResource == nil { + k.lastCommandPerResource = map[kube.ResourceKey]string{} + } + k.lastCommandPerResource[key] = cmd + k.recordLock.Unlock() } func (k *MockKubectlCmd) SetLastValidate(validate bool) { - k.lastValidateLock.Lock() + k.recordLock.Lock() k.lastValidate = validate - k.lastValidateLock.Unlock() + k.recordLock.Unlock() } func (k *MockKubectlCmd) GetLastValidate() bool { - k.lastValidateLock.RLock() + k.recordLock.RLock() validate := k.lastValidate - k.lastValidateLock.RUnlock() + k.recordLock.RUnlock() return validate } @@ -69,9 +89,28 @@ func (k *MockKubectlCmd) DeleteResource(ctx context.Context, config *rest.Config return command.Err } +func (k *MockKubectlCmd) CreateResource(ctx context.Context, config *rest.Config, obj *unstructured.Unstructured, namespace string, dryRunStrategy cmdutil.DryRunStrategy) (*unstructured.Unstructured, error) { + k.SetLastResourceCommand(kube.GetResourceKey(obj), "create") + command, ok := k.Commands[obj.GetName()] + if !ok { + return obj, nil + } + return obj, command.Err +} + func (k *MockKubectlCmd) ApplyResource(ctx context.Context, config *rest.Config, obj *unstructured.Unstructured, namespace string, dryRunStrategy cmdutil.DryRunStrategy, force, validate bool) (string, error) { k.SetLastValidate(validate) + k.SetLastResourceCommand(kube.GetResourceKey(obj), "apply") + command, ok := k.Commands[obj.GetName()] + if !ok { + return "", nil + } + return command.Output, command.Err +} + +func (k *MockKubectlCmd) ReplaceResource(ctx context.Context, config *rest.Config, obj *unstructured.Unstructured, namespace string, dryRunStrategy cmdutil.DryRunStrategy, force bool) (string, error) { command, ok := k.Commands[obj.GetName()] + k.SetLastResourceCommand(kube.GetResourceKey(obj), "replace") if !ok { return "", nil }