From 6c1488222ae0fdbdc063410892478d3b1721ab10 Mon Sep 17 00:00:00 2001 From: rick <1450685+LinuxSuRen@users.noreply.github.com> Date: Wed, 2 Mar 2022 13:41:50 +0800 Subject: [PATCH] Support to update argo cd application --- docs/app.md | 42 +++ go.mod | 4 + go.sum | 37 +++ kubectl-plugin/app/client.go | 120 +++++++++ kubectl-plugin/app/root.go | 17 ++ kubectl-plugin/app/update.go | 423 ++++++++++++++++++++++++++++++ kubectl-plugin/app/update_test.go | 81 ++++++ kubectl-plugin/common/exec.go | 9 +- kubectl-plugin/entrypoint/cmd.go | 2 + kubectl-plugin/types/schema.go | 9 + 10 files changed, 741 insertions(+), 3 deletions(-) create mode 100644 docs/app.md create mode 100644 kubectl-plugin/app/client.go create mode 100644 kubectl-plugin/app/root.go create mode 100644 kubectl-plugin/app/update.go create mode 100644 kubectl-plugin/app/update_test.go diff --git a/docs/app.md b/docs/app.md new file mode 100644 index 0000000..a335115 --- /dev/null +++ b/docs/app.md @@ -0,0 +1,42 @@ +The purpose of the sub-command `ks app` is to update the Argo CD Application git repository. + +This command will do the following steps: + +* Clone the target git repository +* Update the kustomization YAML file\ +* Commit and push the changes to target branch + +## Restriction +Only support kustomization for now. + +## Usage + +Use git username and password directly: +```shell +ks app update --app-name app --app-namespace default \ + --name good --newName good-new \ + --git-password glpat-ULXLsjmC1t6zzFFHtBsD --git-username=linuxsuren1 \ + --git-target-branch test +``` + +Take the git auth from a Kubernetes Secret: +```shell +ks app update --app-name app --app-namespace default \ + --name good --newName good-new1 \ + --secret-namespace default --secret-name gitlab +``` + +## Supported Secrets + +Basic auth type of Secret: +```yaml +apiVersion: v1 +kind: Secret +type: "kubernetes.io/basic-auth" +metadata: + creationTimestamp: null + name: github +stringData: + username: linuxsuren + password: token-or-password +``` diff --git a/go.mod b/go.mod index 5405135..a2d8845 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,9 @@ require ( github.com/Pallinder/go-randomdata v1.2.0 github.com/evanphx/json-patch v4.9.0+incompatible github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 + github.com/go-git/go-git/v5 v5.4.2 github.com/huandu/xstrings v1.3.2 // indirect + github.com/jenkins-x/go-scm v1.11.1 github.com/linuxsuren/cobra-extension v0.0.11 github.com/linuxsuren/go-cli-alias v0.0.8 github.com/linuxsuren/http-downloader v0.0.35 @@ -20,9 +22,11 @@ require ( github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.0 + golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect golang.org/x/text v0.3.7 // indirect + gopkg.in/src-d/go-git.v4 v4.13.1 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b k8s.io/api v0.19.4 k8s.io/apimachinery v0.19.4 diff --git a/go.sum b/go.sum index f8cc867..37544af 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,7 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +code.gitea.io/sdk/gitea v0.14.0/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AlecAivazis/survey/v2 v2.2.2/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk= github.com/AlecAivazis/survey/v2 v2.3.1 h1:lzkuHA60pER7L4eYL8qQJor4bUWlJe4V0gqAT19tdOA= @@ -79,6 +80,7 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -97,6 +99,7 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/bluekeyes/go-gitdiff v0.4.0/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= @@ -127,6 +130,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= @@ -140,6 +144,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -193,12 +198,14 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +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/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -273,6 +280,7 @@ 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= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -288,6 +296,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -301,6 +311,7 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -323,6 +334,8 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jenkins-x/go-scm v1.11.1 h1:732rlXM1x/vTS+7U8I46i0GzAPJf83NUYVw6JAFMA9o= +github.com/jenkins-x/go-scm v1.11.1/go.mod h1:z7xTO9/VzqW3xEbEMH2z5cpOGrZ8+nOHOWfU1ngFGxs= github.com/jenkins-zh/jenkins-cli v0.0.32/go.mod h1:uE1mH9PNITrg0sugv6HXuM/CSddg0zxXoYu3w57I3JY= github.com/jenkins-zh/jenkins-formulas v0.0.5/go.mod h1:zS8fm8u5L6FcjZM0QznXsLV9T2UtSVK+hT6Sm76iUZ4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -402,6 +415,7 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1f github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.1.1 h1:Bp6x9R1Wn16SIz3OfeDr0b7RnCG2OB66Y7PQyC/cvq4= github.com/mitchellh/copystructure v1.1.1/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -413,6 +427,7 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -424,6 +439,7 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -431,6 +447,7 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= @@ -438,6 +455,7 @@ github.com/onsi/ginkgo v1.15.1/go.mod h1:Dd6YFfwBW84ETqqtL0CPyPXillHgY6XhQH3uuCC github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= @@ -484,9 +502,14 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shurcooL/githubv4 v0.0.0-20190718010115-4ba037080260 h1:xKXiRdBUtMVp64NaxACcyX4kvfmHJ9KrLU+JvyB1mdM= +github.com/shurcooL/githubv4 v0.0.0-20190718010115-4ba037080260/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= +github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk= +github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -513,6 +536,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -631,6 +655,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/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-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -696,6 +721,7 @@ golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -945,13 +971,20 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/h2non/gentleman.v1 v1.0.4/go.mod h1:JYuHVdFzS4MKOXe0o+chKJ4hCe6tqKKw9XH9YP6WFrg= +gopkg.in/h2non/gock.v1 v1.0.16 h1:F11k+OafeuFENsjei5t2vMTSTs9L62AdyTe4E1cgdG8= +gopkg.in/h2non/gock.v1 v1.0.16/go.mod h1:XVuDAssexPLwgxCLMvDTWNU5eqklsydR6I5phZ9oPB8= +gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -978,6 +1011,7 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.19.4 h1:I+1I4cgJYuCDgiLNjKx7SLmIbwgj9w7N7Zr5vSIdwpo= k8s.io/api v0.19.4/go.mod h1:SbtJ2aHCItirzdJ36YslycFNzWADYH3tgOhvBEFtZAk= +k8s.io/apimachinery v0.0.0-20190703205208-4cfb76a8bf76/go.mod h1:M2fZgZL9DbLfeJaPBCDqSqNsdsmLN+V29knYJnIXlMA= k8s.io/apimachinery v0.19.4 h1:+ZoddM7nbzrDCp0T3SWnyxqf8cbWPT2fkZImoyvHUG0= k8s.io/apimachinery v0.19.4/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= k8s.io/cli-runtime v0.19.4 h1:FPpoqFbWsFzRbZNRI+o/+iiLFmWMYTmBueIj3OaNVTI= @@ -985,9 +1019,12 @@ k8s.io/cli-runtime v0.19.4/go.mod h1:m8G32dVbKOeaX1foGhleLEvNd6REvU7YnZyWn5//9rw k8s.io/client-go v0.19.4 h1:85D3mDNoLF+xqpyE9Dh/OtrJDyJrSRKkHmDXIbEzer8= k8s.io/client-go v0.19.4/go.mod h1:ZrEy7+wj9PjH5VMBCuu/BDlvtUAku0oVFk4MmnW9mWA= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68= +k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 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/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6 h1:+WnxoVtG8TMiudHBSEtrVL1egv36TkkJm+bA8AxicmQ= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= k8s.io/utils v0.0.0-20200729134348-d5654de09c73 h1:uJmqzgNWG7XyClnU/mLPBWwfKKF1K8Hf8whTseBgJcg= diff --git a/kubectl-plugin/app/client.go b/kubectl-plugin/app/client.go new file mode 100644 index 0000000..c4d907e --- /dev/null +++ b/kubectl-plugin/app/client.go @@ -0,0 +1,120 @@ +/* +Copyright 2022 The KubeSphere Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package app + +import ( + "context" + "errors" + "fmt" + goscm "github.com/jenkins-x/go-scm/scm" + "github.com/jenkins-x/go-scm/scm/driver/github" + "github.com/jenkins-x/go-scm/scm/driver/gitlab" + "github.com/jenkins-x/go-scm/scm/transport/oauth2" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "net/http" +) + +// ClientFactory responsible for creating a git client +type ClientFactory struct { + provider string + secretRef *v1.SecretReference + k8sClient ResourceGetter + token string +} + +// NewClientFactory creates an instance of the ClientFactory +func NewClientFactory(provider string, token string, secretRef *v1.SecretReference, k8sClient ResourceGetter) *ClientFactory { + return &ClientFactory{ + provider: provider, + secretRef: secretRef, + k8sClient: k8sClient, + token: token, + } +} + +// GetClient returns the git client with auth +func (c *ClientFactory) GetClient() (client *goscm.Client, err error) { + switch c.provider { + case "github": + client = github.NewDefault() + case "gitlab": + client = gitlab.NewDefault() + default: + err = errors.New("not support git provider: " + c.provider) + return + } + + var token string + if token, err = c.getToken(); err != nil { + return + } + + client.Client = &http.Client{ + Transport: &oauth2.Transport{ + Source: oauth2.StaticTokenSource( + &goscm.Token{ + Token: token, + }, + ), + }, + } + return +} + +func (c *ClientFactory) getToken() (token string, err error) { + if c.token != "" { + token = c.token + } else if c.secretRef != nil { + token, err = c.getTokenFromSecret(c.secretRef) + } + return +} + +func (c *ClientFactory) getTokenFromSecret(secretRef *v1.SecretReference) (token string, err error) { + var gitSecret *v1.Secret + if gitSecret, err = c.getSecret(secretRef); err != nil { + return + } + + switch gitSecret.Type { + case v1.SecretTypeBasicAuth: + token = string(gitSecret.Data[v1.BasicAuthPasswordKey]) + case v1.SecretTypeOpaque: + token = string(gitSecret.Data[v1.ServiceAccountTokenKey]) + } + return +} + +// getSecret returns the secret, taking the namespace from GitRepository if it is empty +func (c *ClientFactory) getSecret(ref *v1.SecretReference) (secret *v1.Secret, err error) { + secret = &v1.Secret{} + ns := ref.Namespace + + if err = c.k8sClient.Get(context.TODO(), types.NamespacedName{ + Namespace: ns, Name: ref.Name, + }, secret); err != nil { + err = fmt.Errorf("cannot get secret %v, error is: %v", secret, err) + } + return +} + +// ResourceGetter represent the interface for getting Kubernetes resource +type ResourceGetter interface { + Get(ctx context.Context, key types.NamespacedName, obj runtime.Object) error +} diff --git a/kubectl-plugin/app/root.go b/kubectl-plugin/app/root.go new file mode 100644 index 0000000..88296a0 --- /dev/null +++ b/kubectl-plugin/app/root.go @@ -0,0 +1,17 @@ +package app + +import ( + "github.com/spf13/cobra" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" +) + +func NewAppCmd(client dynamic.Interface, clientset *kubernetes.Clientset) (cmd *cobra.Command) { + cmd = &cobra.Command{ + Use: "app", + Short: "Manage applications as the GitOps way", + } + + cmd.AddCommand(newUpdateCmd(client, clientset)) + return +} diff --git a/kubectl-plugin/app/update.go b/kubectl-plugin/app/update.go new file mode 100644 index 0000000..c1dd46d --- /dev/null +++ b/kubectl-plugin/app/update.go @@ -0,0 +1,423 @@ +package app + +import ( + "context" + "fmt" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/jenkins-x/go-scm/scm" + "github.com/kubesphere-sigs/ks/kubectl-plugin/common" + "github.com/kubesphere-sigs/ks/kubectl-plugin/types" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh" + githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http" + gitssh "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "os" + "path" + "strings" + "time" +) + +func newUpdateCmd(client dynamic.Interface, clientset *kubernetes.Clientset) (cmd *cobra.Command) { + opt := updateOption{client: client, clientset: clientset} + cmd = &cobra.Command{ + Use: "update", + Aliases: []string{"up"}, + Short: "Update the files which in the git repository. Support kustomize only.", + PreRunE: opt.preRunE, + RunE: opt.runE, + } + + flags := cmd.Flags() + flags.StringVarP(&opt.appName, "app-name", "", "", + "The name of the application") + flags.StringVarP(&opt.appNamespace, "app-namespace", "", "", + "The namespace of the application") + flags.StringVarP(&opt.name, "name", "n", "", + "Name is a tag-less image name") + flags.StringVarP(&opt.newName, "newName", "", "", + "NewName is the value used to replace the original name") + flags.StringVarP(&opt.newTag, "newTag", "t", "", + "NewTag is the value used to replace the original tag") + flags.StringVarP(&opt.digest, "digest", "d", "", + "Digest is the value used to replace the original image tag. If digest is present NewTag value is ignored") + flags.StringVarP(&opt.mode, "mode", "m", "commit", + "The way you want to push the changes to the target git repository, support mode: pr, commit") + flags.StringVarP(&opt.gitProvider, "git-provider", "", "", + "The flag --mode=pr need the git provider, the mode will fallback to commit if the git provider is empty") + flags.StringVarP(&opt.gitPassword, "git-password", "", "", + "The password of the git provider") + flags.StringVarP(&opt.gitUsername, "git-username", "", "", + "The username of the git provider") + flags.StringVarP(&opt.gitTargetBranch, "git-target-branch", "", "", + "The target branch name that you want to push") + flags.StringVarP(&opt.secretName, "secret-name", "", "", + "The username of the git provider") + flags.StringVarP(&opt.secretNamespace, "secret-namespace", "", "", + "The username of the git provider") + + cmd.MarkFlagRequired("app-name") + cmd.MarkFlagRequired("app-namespace") + cmd.Flags().MarkHidden("mode") + return +} + +func (o *updateOption) preRunE(_ *cobra.Command, _ []string) (err error) { + switch o.mode { + case "pr", "commit": + default: + err = fmt.Errorf("supportted value: pr, commit. Please check the flag --mode") + return + } + return +} + +func (o *updateOption) runE(cmd *cobra.Command, args []string) (err error) { + // find the app cr + var app *application + if app, err = getApplication(o.appName, o.appNamespace, o.client); err != nil { + err = fmt.Errorf("cannot find application '%s/%s', error is: %v", o.appNamespace, o.appName, err) + return + } + + var gitAuth transport.AuthMethod + if gitAuth, err = o.getGitAuth(); err != nil { + err = fmt.Errorf("failed to create git auth, error is: %v", err) + return + } + + // clone the target git repo + var tempDir string + if tempDir, err = cloneGitRepo(app, gitAuth); err != nil { + err = fmt.Errorf("failed to clone git repository '%s', error is: %v", app.gitRepo, err) + return + } + + // run kustomize command + if err = updateKustomization(o, path.Join(tempDir, app.directory)); err != nil { + err = fmt.Errorf("failed to update the kustomization, error is: %v", err) + return + } + + // git commit the changes + forkAppRepo := getForkAppRepo(app) + forkAppRepo.localGitRepoDir = tempDir + forkAppRepo.token = o.gitPassword + forkAppRepo.username = o.gitUsername + forkAppRepo.gitAuth = gitAuth + forkAppRepo.gitTargetBranch = o.gitTargetBranch + if o.gitUsername != "" { + forkAppRepo.botOrg = o.gitUsername + } + if err = pushChanges(o.mode, forkAppRepo); err != nil { + err = fmt.Errorf("failed to push changes, error is: %v", err) + } + return +} + +func (o *updateOption) getGitAuth() (auth transport.AuthMethod, err error) { + if o.gitUsername != "" && o.gitPassword != "" { + auth = getGitUsernameAndPasswordAuth(o.gitUsername, o.gitPassword) + } else if o.secretNamespace != "" && o.secretName != "" { + var secret *v1.Secret + if secret, err = o.clientset.CoreV1().Secrets(o.secretNamespace). + Get(context.Background(), o.secretName, metav1.GetOptions{}); err != nil { + return + } + + auth = getAuth(secret) + } + return +} + +func getForkAppRepo(app *application) *forkAppRepo { + var gitProvider string + var org string + var repo string + + if strings.Contains(app.gitRepo, "github.com") { + gitProvider = "github" + } else if strings.Contains(app.gitRepo, "gitlab.com") { + gitProvider = "gitlab" + } else if strings.Contains(app.gitRepo, "gitee.com") { + gitProvider = "gitee" + } else if strings.Contains(app.gitRepo, "gitea.com") { + gitProvider = "gitea" + } else if strings.Contains(app.gitRepo, "bitbucket.com") { + gitProvider = "bitbucket" + } + + if gitProvider != "" { + orgAndRepo := strings.ReplaceAll(app.gitRepo, fmt.Sprintf("https://%s.com/", gitProvider), "") + orgAndRepo = strings.TrimSuffix(orgAndRepo, "/") + org = strings.Split(orgAndRepo, "/")[0] + repo = strings.Split(orgAndRepo, "/")[1] + } + + return &forkAppRepo{ + application: app, + gitProvider: gitProvider, + org: org, + repo: repo, + forkRemote: "bot", // TODO change it later + botOrg: "linuxsuren-bot", // TODO change it later + } +} + +func getApplication(name, namespace string, client dynamic.Interface) (app *application, err error) { + var unstructedObject *unstructured.Unstructured + if unstructedObject, err = client.Resource(types.GetApplicationSchema()).Namespace(namespace). + Get(context.Background(), name, metav1.GetOptions{}); err != nil { + return + } + + gitRepo, _, _ := unstructured.NestedString(unstructedObject.Object, "spec", "argoApp", "source", "repoURL") + branch, _, _ := unstructured.NestedString(unstructedObject.Object, "spec", "argoApp", "source", "targetRevision") + directory, _, _ := unstructured.NestedString(unstructedObject.Object, "spec", "argoApp", "source", "path") + + app = &application{ + namespace: namespace, + name: name, + gitRepo: gitRepo, + branch: branch, + directory: directory, + } + if app.gitRepo == "" || app.branch == "" || app.directory == "" { + app = nil + err = fmt.Errorf("only support git repository as the source") + } + return +} + +// cloneGitRepo clones the git repository with a generic git client +func cloneGitRepo(app *application, auth transport.AuthMethod) (tempDir string, err error) { + if tempDir, err = os.MkdirTemp(os.TempDir(), ""); err != nil { + err = fmt.Errorf("failed to create a temp directory, error is %v", err) + return + } + if _, err = git.PlainClone(tempDir, false, &git.CloneOptions{ + URL: app.gitRepo, + ReferenceName: plumbing.ReferenceName(app.branch), + Auth: auth, + }); err != nil { + err = fmt.Errorf("failed to clone git repository '%s' into '%s', error: %v", + app.gitRepo, tempDir, err) + } + return +} + +func updateKustomization(patch *updateOption, dir string) (err error) { + var digest string + if patch.digest != "" { + digest = fmt.Sprintf("@%s", patch.digest) + } + var tag string + if patch.newTag != "" { + tag = fmt.Sprintf(":%s", patch.newTag) + } + + imagePatch := fmt.Sprintf("%s=%s%s%s", patch.name, patch.newName, tag, digest) + _ = common.ExecCommandInDirectory("kustomize", dir, "edit", "set", "image", imagePatch) + return +} + +func pushToBranch(forkApp *forkAppRepo, mode string) (err error) { + // add a bot account + var repo *git.Repository + if repo, err = git.PlainOpen(forkApp.localGitRepoDir); err == nil { + var wd *git.Worktree + + if wd, err = repo.Worktree(); err == nil { + if mode == "pr" { + if err = makeSureRemote(forkApp.forkRemote, forkApp.forkRepoAddress, repo); err != nil { + return + } + + if err = wd.Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName(forkApp.forkBranch), + Create: false, + Keep: true, + }); err != nil { + err = fmt.Errorf("unable to checkout git branch: %s, error: %v", forkApp.forkBranch, err) + return + } + } else if forkApp.gitTargetBranch != "" { + if err = wd.Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName(forkApp.gitTargetBranch), + Create: true, + Keep: true, + }); err != nil { + err = fmt.Errorf("unable to checkout git branch: %s, error: %v", forkApp.forkBranch, err) + return + } + } + + // commit and push + if _, err = wd.Add("."); err != nil { + err = fmt.Errorf("failed to run git add command, error is %v", err) + return + } + if _, err = wd.Commit("msg", &git.CommitOptions{ + Committer: nil, + }); err != nil { + err = fmt.Errorf("failed to commit changes, error is %v", err) + return + } + err = repo.Push(&git.PushOptions{ + Auth: forkApp.gitAuth, + }) + } + } + return +} + +func makeSureRemote(name, repoAddr string, repo *git.Repository) (err error) { + if _, err = repo.Remote(name); err != nil { + _, err = repo.CreateRemote(&config.RemoteConfig{ + Name: name, + URLs: []string{repoAddr}, + }) + } + return +} + +func getGitAuthAsToken(token string) transport.AuthMethod { + secret := &v1.Secret{ + Type: v1.SecretTypeOpaque, + Data: map[string][]byte{ + v1.ServiceAccountTokenKey: []byte(token), + }, + } + return getAuth(secret) +} + +func getGitUsernameAndPasswordAuth(username, password string) transport.AuthMethod { + secret := &v1.Secret{ + Type: v1.SecretTypeBasicAuth, + Data: map[string][]byte{ + v1.BasicAuthUsernameKey: []byte(username), + v1.BasicAuthPasswordKey: []byte(password), + }, + } + return getAuth(secret) +} + +func getAuth(secret *v1.Secret) (auth transport.AuthMethod) { + if secret == nil { + return + } + + switch secret.Type { + case v1.SecretTypeOpaque: + auth = &githttp.TokenAuth{ + Token: string(secret.Data[v1.ServiceAccountTokenKey]), + } + case v1.SecretTypeBasicAuth: + auth = &githttp.BasicAuth{ + Username: string(secret.Data[v1.BasicAuthUsernameKey]), + Password: string(secret.Data[v1.BasicAuthPasswordKey]), + } + case v1.SecretTypeSSHAuth: + signer, _ := ssh.ParsePrivateKey(secret.Data[v1.SSHAuthPrivateKey]) + auth = &gitssh.PublicKeys{User: "git", Signer: signer} + } + return +} + +func pushChanges(mode string, forkApp *forkAppRepo) (err error) { + provider := forkApp.gitProvider + repo := forkApp.repo + ns := forkApp.org + botNs := forkApp.botOrg + orgAndRepo := fmt.Sprintf("%s/%s", ns, repo) + branchName := fmt.Sprintf("%d", time.Now().Nanosecond()) + forkApp.forkBranch = branchName + + if mode == "pr" { + var client *scm.Client + if client, err = NewClientFactory(provider, forkApp.token, nil, nil).GetClient(); err == nil && client != nil { + in := &scm.RepositoryInput{ + Namespace: botNs, + } + var forkedRepo *scm.Repository + if forkedRepo, _, err = client.Repositories.Fork(context.Background(), in, orgAndRepo); err != nil { + err = fmt.Errorf("failed to fork repo: %s, error is: %v", orgAndRepo, err) + return + } + + forkApp.forkRepoAddress = forkedRepo.Clone + if err = pushToBranch(forkApp, mode); err != nil { + err = fmt.Errorf("failed to push branch, error is: %v", err) + return + } + + input := &scm.PullRequestInput{ + Title: "Amazing new feature", + Body: "Please pull these awesome changes in!", + Head: fmt.Sprintf("%s:%s", botNs, branchName), + Base: "master", + } + + _, _, err = client.PullRequests.Create(context.Background(), orgAndRepo, input) + } + } else { + if err = pushToBranch(forkApp, mode); err != nil { + err = fmt.Errorf("failed to push branch, error is: %v", err) + return + } + } + return +} + +type forkAppRepo struct { + *application + + gitProvider string + gitTargetBranch string + org, repo string + botOrg string + localGitRepoDir string + forkRemote string + forkRepoAddress string + forkBranch string + token string + username string + gitAuth transport.AuthMethod +} + +type application struct { + namespace string + name string + gitRepo string + branch string + directory string +} + +type updateOption struct { + appName string + appNamespace string + name string + newName string + newTag string + digest string + // mode indicates how to update the application git repository, pr or commit + mode string + gitProvider string + gitPassword string + gitUsername string + gitTargetBranch string + + // secretName and secretNamespace are used to the git auth + secretName string + secretNamespace string + + client dynamic.Interface + clientset *kubernetes.Clientset +} diff --git a/kubectl-plugin/app/update_test.go b/kubectl-plugin/app/update_test.go new file mode 100644 index 0000000..2c95453 --- /dev/null +++ b/kubectl-plugin/app/update_test.go @@ -0,0 +1,81 @@ +package app + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/fake" + "reflect" + "testing" +) + +func Test_getApplication(t *testing.T) { + defaultApp := &unstructured.Unstructured{} + defaultApp.SetKind("Application") + defaultApp.SetAPIVersion("gitops.kubesphere.io/v1alpha1") + defaultApp.SetNamespace("ns") + defaultApp.SetName("fake") + _ = unstructured.SetNestedField(defaultApp.Object, "http://git.com", "spec", "argoApp", "source", "repoURL") + _ = unstructured.SetNestedField(defaultApp.Object, "master", "spec", "argoApp", "source", "targetRevision") + _ = unstructured.SetNestedField(defaultApp.Object, "current", "spec", "argoApp", "source", "path") + + invalidApp := defaultApp.DeepCopy() + _ = unstructured.SetNestedField(invalidApp.Object, "", "spec", "argoApp", "source", "repoURL") + + scheme := runtime.NewScheme() + + type args struct { + name string + namespace string + client dynamic.Interface + } + tests := []struct { + name string + args args + wantApp *application + wantErr bool + }{{ + name: "normal case", + args: args{ + name: "fake", + namespace: "ns", + client: fake.NewSimpleDynamicClient(scheme, defaultApp.DeepCopy()), + }, + wantApp: &application{ + namespace: "ns", + name: "fake", + gitRepo: "http://git.com", + branch: "master", + directory: "current", + }, + wantErr: false, + }, { + name: "invalid application", + args: args{ + name: "fake", + namespace: "ns", + client: fake.NewSimpleDynamicClient(scheme, invalidApp.DeepCopy()), + }, + wantErr: true, + }, { + name: "not found application", + args: args{ + name: "fake", + namespace: "ns", + client: fake.NewSimpleDynamicClient(scheme), + }, + wantErr: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotApp, err := getApplication(tt.args.name, tt.args.namespace, tt.args.client) + if (err != nil) != tt.wantErr { + t.Errorf("getApplication() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotApp, tt.wantApp) { + t.Errorf("getApplication() gotApp = %v, want %v", gotApp, tt.wantApp) + } + }) + } +} diff --git a/kubectl-plugin/common/exec.go b/kubectl-plugin/common/exec.go index ab2b442..b4721cf 100644 --- a/kubectl-plugin/common/exec.go +++ b/kubectl-plugin/common/exec.go @@ -8,11 +8,14 @@ import ( ) // ExecCommand runs a command -func ExecCommand(name string, arg ...string) (err error) { +func ExecCommand(name string, arg ...string) error { + return ExecCommandInDirectory(name, "", arg...) +} + +func ExecCommandInDirectory(name, dir string, arg ...string) (err error) { command := exec.Command(name, arg...) + command.Dir = dir - //var stdout []byte - //var errStdout error stdoutIn, _ := command.StdoutPipe() stderrIn, _ := command.StderrPipe() err = command.Start() diff --git a/kubectl-plugin/entrypoint/cmd.go b/kubectl-plugin/entrypoint/cmd.go index 4c8966a..d758146 100644 --- a/kubectl-plugin/entrypoint/cmd.go +++ b/kubectl-plugin/entrypoint/cmd.go @@ -2,6 +2,7 @@ package entrypoint import ( "fmt" + "github.com/kubesphere-sigs/ks/kubectl-plugin/app" "github.com/kubesphere-sigs/ks/kubectl-plugin/auth" "github.com/kubesphere-sigs/ks/kubectl-plugin/common" "github.com/kubesphere-sigs/ks/kubectl-plugin/component" @@ -68,6 +69,7 @@ See also https://github.com/kubesphere/kubesphere`, install.NewInstallCmd(), config.NewConfigRootCmd(client), source2image.NewS2ICmd(client), + app.NewAppCmd(client, clientSet), hdcmd.NewInitCommand(map[string]string{ "kubectl": "kubectl", }, map[string]string{ diff --git a/kubectl-plugin/types/schema.go b/kubectl-plugin/types/schema.go index b497346..d77fb74 100644 --- a/kubectl-plugin/types/schema.go +++ b/kubectl-plugin/types/schema.go @@ -141,3 +141,12 @@ func GetS2iBuilderSchema() schema.GroupVersionResource { Resource: "s2ibuilders", } } + +// GetApplicationSchema returns the schema of Application +func GetApplicationSchema() schema.GroupVersionResource { + return schema.GroupVersionResource{ + Group: "gitops.kubesphere.io", + Version: "v1alpha1", + Resource: "applications", + } +}