From f3ca02a96a5de894cf541bcffa67e63194f54629 Mon Sep 17 00:00:00 2001 From: Jingfang Liu Date: Mon, 13 May 2019 16:23:58 -0700 Subject: [PATCH] address comments and add tests for pruning --- Dockerfile | 28 ++ cmd/apply/apply.go | 14 +- cmd/cmd.go | 6 +- cmd/delete/delete.go | 13 +- cmd/prune/prune.go | 46 ++- go.mod | 1 - go.sum | 13 + internal/pkg/apply/apply.go | 20 +- internal/pkg/client/client_test.go | 10 +- internal/pkg/delete/delete.go | 39 ++- internal/pkg/delete/delete_test.go | 12 +- internal/pkg/prune/doc.go | 76 ++++ internal/pkg/prune/prune.go | 30 +- internal/pkg/prune/prune_test.go | 544 ++++++++++++++++++++++++++++- internal/pkg/wirecli/wirecli.go | 2 + 15 files changed, 805 insertions(+), 49 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..21c5048 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM golang:alpine AS builder + +# Install git +RUN apk update && apk add --no-cache git + +WORKDIR $GOPATH/src/cli-experimental +COPY . . + +# Install wire +RUN GO111MODULE=off go get github.com/google/wire/cmd/wire + +ENV CGO_ENABLED=0 + +# Run go generate +RUN GO111MODULE=on go generate + +# Build binary +RUN GO111MODULE=on go build -o /go/bin/k2 + +FROM scratch + +# Copy the executable. +COPY --from=builder /go/bin/k2 /go/bin/k2 + +# Run the binary +ENTRYPOINT ["go/bin/k2"] + + diff --git a/cmd/apply/apply.go b/cmd/apply/apply.go index 8b14887..cd00b2d 100644 --- a/cmd/apply/apply.go +++ b/cmd/apply/apply.go @@ -30,8 +30,18 @@ func GetApplyCommand(a util.Args) *cobra.Command { cmd := &cobra.Command{ Use: "apply", Short: "Apply resource configurations.", - Long: `Apply resource configurations to k8s cluster.`, - Args: cobra.MinimumNArgs(1), + Long: `Apply resource configurations to k8s cluster. +The resource configurations can be from a Kustomization directory. +The path of the resource configurations should be passed to apply +as an argument. + + # Apply the configurations from a directory containing kustomization.yaml - e.g. dir/kustomization.yaml + k2 apply dir + +When server-side apply is available on the cluster, it is used; otherwise, client-side apply +is used. +`, + Args: cobra.MinimumNArgs(1), } cmd.RunE = func(cmd *cobra.Command, args []string) error { diff --git a/cmd/cmd.go b/cmd/cmd.go index 4222305..53f36ee 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -28,9 +28,11 @@ import ( // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute(args []string, fn func(*cobra.Command)) error { rootCmd := &cobra.Command{ - Use: "k2", + // TODO(Liujingfang1): change this binary to a better name + Use: "cli-experimental", Short: "kubectl version 2", - Long: `kubectl version 2`, + Long: `kubectl version 2 +with commands apply, prune, delete and dynamic commands`, } if fn != nil { fn(rootCmd) diff --git a/cmd/delete/delete.go b/cmd/delete/delete.go index 41d6925..8693635 100644 --- a/cmd/delete/delete.go +++ b/cmd/delete/delete.go @@ -26,9 +26,16 @@ import ( func GetDeleteCommand(a util.Args) *cobra.Command { cmd := &cobra.Command{ Use: "delete", - Short: "Delete resources from a K8s cluster.", - Long: `Delete resources from a K8s cluster.`, - Args: cobra.MinimumNArgs(1), + Short: "Delete resources from a Kubernetes cluster.", + Long: `Delete resources from a Kubernetes cluster. +The resource configurations can be from a Kustomization directory. +The path of the resource configurations should be passed to delete +as an argument. + + # Delete the configurations from a directory containing kustomization.yaml - e.g. dir/kustomization.yaml + k2 delete dir +`, + Args: cobra.MinimumNArgs(1), } cmd.RunE = func(cmd *cobra.Command, args []string) error { diff --git a/cmd/prune/prune.go b/cmd/prune/prune.go index aef897c..04912b3 100644 --- a/cmd/prune/prune.go +++ b/cmd/prune/prune.go @@ -28,9 +28,49 @@ func GetPruneCommand(a util.Args) *cobra.Command { Use: "prune", Short: "Prune obsolete resources.", Long: `Prune obsolete resources. - It is based on checking the inventory annotation that - is stored in the resource configuration that is passed - to prune command.`, + # Prune the configurations from a directory containing kustomization.yaml -e.g. dir/kustomization.yaml + k2 prune dir + +The pruning is done based on checking the inventory annotation that +is stored in the resource configuration that is passed to prune command. +The inventory annotation kustomize.k8s.io/Inventory has following format: +{ + "current": + { + "apps_v1_Deployment|default|mysql":null, + "~G_v1_Secret|default|pass-dfg7h97cf6": + [ + { + "group":"apps", + "version":"v1", + "kind":"Deployment", + "name":"mysql", + "namespace":"default", + } + ], + "~G_v1_Service|default|mysql":null + } + } + "previous: + { + "apps_v1_Deployment|default|mysql":null, + "~G_v1_Secret|default|pass-dfg7h97cf6": + [ + { + "group":"apps", + "version":"v1", + "kind":"Deployment", + "name":"mysql", + "namespace":"default", + } + ], + "~G_v1_Service|default|mysql":null + } + } +} +Any objects in the previous part that don't show up in the current part will be pruned. +For more information, see https://github.com/kubernetes-sigs/kustomize/blob/master/docs/inventory_object.md. +`, Args: cobra.MinimumNArgs(1), } diff --git a/go.mod b/go.mod index a4f51af..19f1b63 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,6 @@ require ( github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect github.com/grpc-ecosystem/grpc-gateway v1.8.5 // indirect github.com/imdario/mergo v0.3.7 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb // indirect github.com/json-iterator/go v1.1.6 // indirect github.com/mdempsky/maligned v0.0.0-20180708014732-6e39bd26a8c8 // indirect diff --git a/go.sum b/go.sum index c8d8d22..8ec3011 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,7 @@ github.com/alexkohler/nakedret v0.0.0-20190321114339-98ae56e4e0f3/go.mod h1:tfDQ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/census-instrumentation/opencensus-proto v0.2.0 h1:LzQXZOgg4CQfE6bFvXGM30YZL1WW/M337pXml+GrcZ4= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -124,6 +125,7 @@ github.com/grpc-ecosystem/grpc-gateway v1.8.5 h1:2+KSC78XiO6Qy0hIjfc1OD9H+hsaJdJ github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= @@ -135,6 +137,7 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb h1:D5s1HIu80AcMGcqmk7fNIVptmAubVHHaj3v5Upex6Zs= github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb/go.mod h1:82TxjOpWQiPmywlbIaB2ZkqJoSYJdLGPgAJDvM3PbKc= +github.com/json-iterator/go v0.0.0-20180315132816-ca39e5af3ece/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -154,6 +157,7 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20180606163543-3fdea8d05856/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -166,6 +170,7 @@ github.com/mibk/dupl v1.0.0/go.mod h1:pCr4pNxxIbFGvtyCOi0c7LVjmV6duhKWV+ex5vh38M github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180228065516-1df9eeb2bb81/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -190,6 +195,7 @@ github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTm github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -215,12 +221,16 @@ github.com/securego/gosec v0.0.0-20190510081509-ee80733faf72/go.mod h1:shk+oGa7J github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 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= @@ -239,6 +249,7 @@ github.com/walle/lll v0.0.0-20160702150637-8b13b3fbf731 h1:b7JCW5NchRssZBlI5RffU github.com/walle/lll v0.0.0-20160702150637-8b13b3fbf731/go.mod h1:OjXnoVXDAiJx16YuOKsfCCcyNAFO+fj/Ocwtvh0K5SU= github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro= github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A= go.opencensus.io v0.19.2 h1:ZZpq6xI6kv/LuE/5s5UQvBU5vMjvRnPb8PvJrIntAnc= go.opencensus.io v0.19.2/go.mod h1:NO/8qkisMZLZ1FCsKNqtJPwc8/TaclWyY0B6wcYNg9M= @@ -249,6 +260,7 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -298,6 +310,7 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= diff --git a/internal/pkg/apply/apply.go b/internal/pkg/apply/apply.go index ee88f6b..7e2bab8 100644 --- a/internal/pkg/apply/apply.go +++ b/internal/pkg/apply/apply.go @@ -31,10 +31,18 @@ import ( // Apply applies directories type Apply struct { + // DynamicClient is the client used to talk + // with the cluster DynamicClient client.Client - Out io.Writer - Resources clik8s.ResourceConfigs - Commit *object.Commit + + // Out stores the output + Out io.Writer + + // Resources is a list of resource configurations + Resources clik8s.ResourceConfigs + + // Commit is a git commit object + Commit *object.Commit } // Result contains the Apply Result @@ -49,7 +57,7 @@ func (a *Apply) Do() (Result, error) { // TODO(Liuijngfang1): add a dry-run for all objects // When the dry-run passes, proceed to the actual apply - for _, u := range adjustOrder(a.Resources) { + for _, u := range normalizeResourceOrdering(a.Resources) { annotation := u.GetAnnotations() _, ok := annotation[inventory.InventoryAnnotation] @@ -113,8 +121,8 @@ func mergeInventoryAnnotation(newObj, oldObj *unstructured.Unstructured) (*unstr return newObj, nil } -// adjustOrder moves the inventory object to be the first resource -func adjustOrder(resources clik8s.ResourceConfigs) []*unstructured.Unstructured { +// normalizeResourceOrdering moves the inventory object to be the first resource +func normalizeResourceOrdering(resources clik8s.ResourceConfigs) []*unstructured.Unstructured { var results []*unstructured.Unstructured index := -1 for i, u := range resources { diff --git a/internal/pkg/client/client_test.go b/internal/pkg/client/client_test.go index 64e8962..cacf207 100644 --- a/internal/pkg/client/client_test.go +++ b/internal/pkg/client/client_test.go @@ -334,7 +334,10 @@ var _ = Describe("Client", func() { By("encoding the deployment as unstructured") u := &unstructured.Unstructured{} scheme.Convert(dep, u, nil) - annotation, err := patch.GetModifiedConfiguration(u, true) + data, err := patch.SerializeLastApplied(u, true) + Expect(err).NotTo(HaveOccurred()) + expected := u.DeepCopy() + err = runtime.DecodeInto(unstructured.UnstructuredJSONScheme, data, expected) Expect(err).NotTo(HaveOccurred()) u.SetGroupVersionKind(schema.GroupVersionKind{ Group: "apps", @@ -349,7 +352,8 @@ var _ = Describe("Client", func() { actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{}) Expect(err).NotTo(HaveOccurred()) Expect(actual).NotTo(BeNil()) - Expect(actual.Annotations[v1.LastAppliedConfigAnnotation]).To(Equal(string(annotation))) + + Expect(actual.Annotations[v1.LastAppliedConfigAnnotation]).To(Equal(expected.GetAnnotations()[v1.LastAppliedConfigAnnotation])) close(done) }) @@ -379,7 +383,7 @@ var _ = Describe("Client", func() { Version: "v1", }) u.SetAnnotations(map[string]string{"foo": "bar"}) - annotation, err := patch.GetModifiedConfiguration(u, false) + annotation, err := patch.SerializeLastApplied(u, false) Expect(err).NotTo(HaveOccurred()) err = cl.Apply(context.TODO(), u) Expect(err).NotTo(HaveOccurred()) diff --git a/internal/pkg/delete/delete.go b/internal/pkg/delete/delete.go index d6fccdc..7b07dfa 100644 --- a/internal/pkg/delete/delete.go +++ b/internal/pkg/delete/delete.go @@ -31,10 +31,18 @@ import ( // Delete applies directories type Delete struct { + // DynamicClient is the client used to talk + // with the cluster DynamicClient client.Client - Out io.Writer - Resources clik8s.ResourceConfigs - Commit *object.Commit + + // Out stores the output + Out io.Writer + + // Resources is a list of resource configurations + Resources clik8s.ResourceConfigs + + // Commit is a git commit object + Commit *object.Commit } // Result contains the Apply Result @@ -45,18 +53,19 @@ type Result struct { // Do executes the delete func (a *Delete) Do() (Result, error) { fmt.Fprintf(a.Out, "Doing `cli-experimental delete`\n") - for _, u := range adjustOrder(a.Resources) { + ctx := context.Background() + for _, u := range normalizeResourceOrdering(a.Resources) { annotations := u.GetAnnotations() _, ok := annotations[inventory.InventoryAnnotation] if ok { - err := a.deleteLeftOvers(annotations) + err := a.handleInventroy(ctx, annotations) if err != nil { fmt.Fprintf(os.Stderr, "failed to delete leftovers for inventory %v\n", err) continue } } - err := a.deleteObject(u.GroupVersionKind(), u.GetNamespace(), u.GetName()) + err := a.deleteObject(ctx, u.GroupVersionKind(), u.GetNamespace(), u.GetName()) if err != nil { fmt.Fprint(os.Stderr, err) } @@ -65,7 +74,12 @@ func (a *Delete) Do() (Result, error) { return Result{Resources: a.Resources}, nil } -func (a *Delete) deleteLeftOvers(annotations map[string]string) error { +// handleInventory reads the inventory annotation +// and delete any object recorded in it that hasn't been deleted. +// When there is an inventory object in the resource configurations, the inventory +// object may record some objects that are applied previously and never been pruned. +// By delete command, those objects are supposed to be cleaned up as well. +func (a *Delete) handleInventroy(ctx context.Context, annotations map[string]string) error { inv := inventory.NewInventory() err := inv.LoadFromAnnotation(annotations) if err != nil { @@ -80,7 +94,7 @@ func (a *Delete) deleteLeftOvers(annotations map[string]string) error { Version: id.Version, Kind: id.Kind, } - err = a.deleteObject(gvk, id.Namespace, id.Name) + err = a.deleteObject(ctx, gvk, id.Namespace, id.Name) if err != nil { fmt.Fprint(os.Stderr, err) } @@ -88,13 +102,13 @@ func (a *Delete) deleteLeftOvers(annotations map[string]string) error { return nil } -func (a *Delete) deleteObject(gvk schema.GroupVersionKind, ns, nm string) error { +func (a *Delete) deleteObject(ctx context.Context, gvk schema.GroupVersionKind, ns, nm string) error { obj := &unstructured.Unstructured{} obj.SetGroupVersionKind(gvk) obj.SetNamespace(ns) obj.SetName(nm) - err := a.DynamicClient.Delete(context.Background(), obj, &metav1.DeleteOptions{}) + err := a.DynamicClient.Delete(ctx, obj, &metav1.DeleteOptions{}) if err != nil { if errors.IsNotFound(err) { return nil @@ -104,8 +118,9 @@ func (a *Delete) deleteObject(gvk schema.GroupVersionKind, ns, nm string) error return nil } -// adjustOrder move the inventory object to be the last resource -func adjustOrder(resources clik8s.ResourceConfigs) []*unstructured.Unstructured { +// normalizeResourceOrdering move the inventory object to be the last resource +// This is to make sure the inventory object is the last object to be deleted. +func normalizeResourceOrdering(resources clik8s.ResourceConfigs) []*unstructured.Unstructured { var results []*unstructured.Unstructured index := -1 for i, u := range resources { diff --git a/internal/pkg/delete/delete_test.go b/internal/pkg/delete/delete_test.go index 3aa81cc..c5b35a3 100644 --- a/internal/pkg/delete/delete_test.go +++ b/internal/pkg/delete/delete_test.go @@ -18,12 +18,10 @@ import ( "context" "testing" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/stretchr/testify/assert" "gopkg.in/src-d/go-git.v4/plumbing/object" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/cli-experimental/internal/pkg/clik8s" "sigs.k8s.io/cli-experimental/internal/pkg/delete" "sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wiretest" @@ -39,7 +37,7 @@ func TestDeleteEmpty(t *testing.T) { assert.Equal(t, delete.Result{}, r) } -func TestPrune(t *testing.T) { +func TestDelete(t *testing.T) { buf := new(bytes.Buffer) kp := wiretest.InitializConfigProvider() fs, cleanup, err := wiretest.InitializeKustomization() @@ -65,7 +63,7 @@ func TestPrune(t *testing.T) { Kind: "ConfigMapList", Version: "v1", }) - err = a.DynamicClient.List(context.Background(), cmList, "default", metav1.ListOptions{}) + err = a.DynamicClient.List(context.Background(), cmList, "default", nil) assert.NoError(t, err) assert.Equal(t, len(cmList.Items), 3) @@ -76,7 +74,7 @@ func TestPrune(t *testing.T) { _, err = d.Do() assert.NoError(t, err) - err = d.DynamicClient.List(context.Background(), cmList, "default", metav1.ListOptions{}) + err = d.DynamicClient.List(context.Background(), cmList, "default", nil) assert.NoError(t, err) assert.Equal(t, len(cmList.Items), 0) } diff --git a/internal/pkg/prune/doc.go b/internal/pkg/prune/doc.go index 798b7ee..aed1991 100644 --- a/internal/pkg/prune/doc.go +++ b/internal/pkg/prune/doc.go @@ -12,3 +12,79 @@ limitations under the License. */ package prune + +/* +Prune delete objects based on the inventory annotation, which has following format + +kustomize.k8s.io/Inventory: +{ + "current": + { + "apps_v1_Deployment|default|mysql":null, + "~G_v1_Secret|default|pass-dfg7h97cf6": + [ + { + "group":"apps", + "version":"v1", + "kind":"Deployment", + "name":"mysql", + "namespace":"default", + } + ], + "~G_v1_Service|default|mysql":null + } + } + "previous: + { + "apps_v1_Deployment|default|mysql":null, + "~G_v1_Secret|default|pass-dfg7h97cf6": + [ + { + "group":"apps", + "version":"v1", + "kind":"Deployment", + "name":"mysql", + "namespace":"default", + } + ], + "~G_v1_Service|default|mysql":null + } + } +} + +Kusotmize.k8s.io/InventoryHash: + + +This inventory annotation is generated automatically by Kustomize when `inventory` directive is +present in the kustomization.yaml file. + +inventory: + type: ConfigMap + configMap: + name: my-inventory + namespace: default + +The inventory object generated by Kustomize only contains "current" part in the annotation. +When this object is passed to apply, prune and delete, it doesn't contains the "previous" part. +The existing inventory object on cluster can contain both the "current" part and "previous" part. + +Apply reads the existing inventory object and merge its "current" into "previous". The "current" is then set +to be the input inventory object's "current". + +Prune reads the existing inventory object and delete any object that satisfies following conditions: + - It does not show up in the "current" part + - It does not referred by any object that shows up in the "current" part + +The tests in this package covers following cases: +- A kustomization with one ConfigMap, which is not referred. When the name of this ConfigMap changes, prune + deletes the previous ConfigMap +- A kustomization with one Secret and one Deployment, where the Secret is referred in the Deployment. + When the name of the Secret changes, prune doesn't delete the previous Secrets since the Deployment + object still exists. +- A kustomization with one Secret and one StatefulSet, where the Secret is referred in the StatefulSet. + The case is the same as the above. When the name of the Secret changes, prune doesn't delete the previous + Secrets. +- A kustomization with multiple objects. When a different namePrefix is added, all objects in that + Kustomization have a different name. Prune deletes the previous set of objects and only keep the objects + with the latest nameprefix. +*/ diff --git a/internal/pkg/prune/prune.go b/internal/pkg/prune/prune.go index 593a9de..cb02d75 100644 --- a/internal/pkg/prune/prune.go +++ b/internal/pkg/prune/prune.go @@ -31,12 +31,22 @@ import ( "sigs.k8s.io/kustomize/pkg/inventory" ) -// Prune prunes directories +// Prune prunes obsolete resources from a kustomization directory +// that are applied in previous applies but not show up in the +// latest apply. type Prune struct { + // DynamicClient is the client used to talk + // with the cluster DynamicClient client.Client - Out io.Writer - Resources clik8s.ResourcePruneConfigs - Commit *object.Commit + + // Out stores the output + Out io.Writer + + // Resources is the resource used for pruning + Resources clik8s.ResourcePruneConfigs + + // Commit is a git commit object + Commit *object.Commit } // Result contains the Prune Result @@ -50,6 +60,7 @@ func (o *Prune) Do() (Result, error) { return Result{}, nil } fmt.Fprintf(o.Out, "Doing `cli-experimental prune`\n") + ctx := context.Background() u := (*unstructured.Unstructured)(o.Resources) annotation := u.GetAnnotations() @@ -59,7 +70,7 @@ func (o *Prune) Do() (Result, error) { } obj := u.DeepCopy() - err := o.DynamicClient.Get(context.Background(), + err := o.DynamicClient.Get(ctx, types.NamespacedName{Namespace: u.GetNamespace(), Name: u.GetName()}, obj) if err != nil { @@ -70,7 +81,7 @@ func (o *Prune) Do() (Result, error) { fmt.Fprintf(os.Stderr, "retrieving current configuration of %s from server for %v", u.GetName(), err) return Result{}, err } - obj, results, err := o.runPrune(obj) + obj, results, err := o.runPrune(ctx, obj) if err != nil { return Result{}, err } @@ -90,7 +101,7 @@ func (o *Prune) Do() (Result, error) { // https://github.com/kubernetes-sigs/kustomize/tree/master/pkg/inventory // This is based on the KEP // https://github.com/kubernetes/enhancements/pull/810 -func (o *Prune) runPrune(obj *unstructured.Unstructured) ( +func (o *Prune) runPrune(ctx context.Context, obj *unstructured.Unstructured) ( *unstructured.Unstructured, []*unstructured.Unstructured, error) { var results []*unstructured.Unstructured annotations := obj.GetAnnotations() @@ -103,7 +114,7 @@ func (o *Prune) runPrune(obj *unstructured.Unstructured) ( Version: item.Version, Kind: item.Kind, } - u, err := o.deleteObject(gvk, item.Namespace, item.Name) + u, err := o.deleteObject(ctx, gvk, item.Namespace, item.Name) if err != nil { return nil, nil, err } @@ -116,7 +127,8 @@ func (o *Prune) runPrune(obj *unstructured.Unstructured) ( return obj, results, nil } -func (o *Prune) deleteObject(gvk schema.GroupVersionKind, ns, nm string) (*unstructured.Unstructured, error) { +func (o *Prune) deleteObject(ctx context.Context, gvk schema.GroupVersionKind, + ns, nm string) (*unstructured.Unstructured, error) { obj := &unstructured.Unstructured{} obj.SetGroupVersionKind(gvk) obj.SetNamespace(ns) diff --git a/internal/pkg/prune/prune_test.go b/internal/pkg/prune/prune_test.go index 1cb5110..579f043 100644 --- a/internal/pkg/prune/prune_test.go +++ b/internal/pkg/prune/prune_test.go @@ -15,10 +15,16 @@ package prune_test import ( "bytes" + "context" + "io/ioutil" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" "gopkg.in/src-d/go-git.v4/plumbing/object" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/cli-experimental/internal/pkg/clik8s" "sigs.k8s.io/cli-experimental/internal/pkg/prune" "sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wiretest" @@ -34,7 +40,12 @@ func TestPruneEmpty(t *testing.T) { assert.Equal(t, prune.Result{}, r) } -func TestPrune(t *testing.T) { +/* TestPruneWithoutInventory takes following steps + 1. create a Kustomization with a ConfigMapGenerator and an inventory object + 6. run prune + 7. confirm that no object is pruned since there is no existing inventory object +*/ +func TestPruneWithoutInventory(t *testing.T) { buf := new(bytes.Buffer) kp := wiretest.InitializConfigProvider() fs, cleanup, err := wiretest.InitializeKustomization() @@ -43,6 +54,36 @@ func TestPrune(t *testing.T) { assert.NoError(t, err) assert.Equal(t, len(fs), 2) + // run the prune + pruneObject, err := kp.GetPruneConfig(fs[1]) + assert.NoError(t, err) + p, donep, err := wiretest.InitializePrune(pruneObject, &object.Commit{}, buf) + defer donep() + assert.NoError(t, err) + pr, err := p.Do() + assert.NoError(t, err) + assert.Equal(t, len(pr.Resources), 0) +} + +/* TestPruneOneObject take following steps + 1. create a Kustomization with a ConfigMapGenerator and an inventory object + 2. apply the kustomization + 3. update the ConfigMapGenerator so that the ConfigMap that gets generated has a different name + 4. apply the kustomization again + 5. confirm that there are 3 ConfigMaps (including the inventroy ConfigMap) + 6. run prune + 7. confirm that there are 2 ConfigMaps (the second ConfigMap and the inventory object) +*/ +func TestPruneOneObject(t *testing.T) { + buf := new(bytes.Buffer) + kp := wiretest.InitializConfigProvider() + fs, cleanup, err := wiretest.InitializeKustomization() + assert.NoError(t, err) + defer cleanup() + assert.NoError(t, err) + assert.Equal(t, len(fs), 2) + + // call apply to create the first configmap objects, err := kp.GetConfig(fs[0]) assert.NoError(t, err) a, donea, err := wiretest.InitializeApply(objects, &object.Commit{}, buf) @@ -50,11 +91,24 @@ func TestPrune(t *testing.T) { defer donea() _, err = a.Do() assert.NoError(t, err) + + // call apply again to create the second configmap a.Resources, err = kp.GetConfig(fs[1]) assert.NoError(t, err) _, err = a.Do() assert.NoError(t, err) + // confirm that there are three ConfigMaps + cmList := &unstructured.UnstructuredList{} + cmList.SetGroupVersionKind(schema.GroupVersionKind{ + Kind: "ConfigMapList", + Version: "v1", + }) + err = a.DynamicClient.List(context.Background(), cmList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(cmList.Items), 3) + + // run the prune pruneObject, err := kp.GetPruneConfig(fs[1]) assert.NoError(t, err) p, donep, err := wiretest.InitializePrune(pruneObject, &object.Commit{}, buf) @@ -64,4 +118,492 @@ func TestPrune(t *testing.T) { pr, err := p.Do() assert.NoError(t, err) assert.Equal(t, len(pr.Resources), 1) + + // confirm that one ConfigMap is deleted. + // There are two ConfigMap left. + err = a.DynamicClient.List(context.Background(), cmList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(cmList.Items), 2) +} + +func setupKustomizeWithDeployment(s string) (string, error) { + f, err := ioutil.TempDir("/tmp", "TestApply") + if err != nil { + return "", err + } + err = ioutil.WriteFile(filepath.Join(f, "kustomization.yaml"), []byte(`apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +`+s+ + ` +resources: +- deployment.yaml + +inventory: + type: ConfigMap + configMap: + name: inventory + namespace: default + +namespace: default +`), 0644) + if err != nil { + return "", err + } + + err = ioutil.WriteFile(filepath.Join(f, "deployment.yaml"), []byte(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql + labels: + app: mysql +spec: + selector: + matchLabels: + app: mysql + strategy: + type: Recreate + template: + metadata: + labels: + app: mysql + spec: + containers: + - image: mysql:5.6 + name: mysql + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: pass + key: password +`), 0644) + if err != nil { + return "", err + } + + return f, nil +} + +/* TestPruneConfigMapWithDeployment take following steps + 1. create a Kustomization with a SecretGenerator, a Deployment + that refers to the generated Secret as well as an inventory object + 2. apply the kustomization + 3. update the SecretGenerator so that the Secret that gets generated + has a different name + 4. apply the kustomization again + 5. confirm that there are 2 Secrets + 6. run prune + 7. confirm that there are 2 Secrets, the first generated Secret is + not deleted since it is referred by the Deployment and the + Deployment object is not removed yet. +*/ +func TestPruneConfigMapWithDeployment(t *testing.T) { + buf := new(bytes.Buffer) + kp := wiretest.InitializConfigProvider() + + // setup the first version of resource configurations + // and run apply + f1, err := setupKustomizeWithDeployment(` +secretGenerator: +- name: pass + literals: + - password=secret +`) + assert.NoError(t, err) + defer os.RemoveAll(f1) + assert.NoError(t, err) + objects, err := kp.GetConfig(f1) + assert.NoError(t, err) + a, donea, err := wiretest.InitializeApply(objects, &object.Commit{}, buf) + assert.NoError(t, err) + defer donea() + _, err = a.Do() + assert.NoError(t, err) + + // setup the second version of resource configurations + // and run apply + f2, err := setupKustomizeWithDeployment(` +secretGenerator: +- name: pass + literals: + - password=secret + - a=b +`) + assert.NoError(t, err) + defer os.RemoveAll(f2) + a.Resources, err = kp.GetConfig(f2) + assert.NoError(t, err) + _, err = a.Do() + assert.NoError(t, err) + + // Confirm that there are two Secrets + sList := &unstructured.UnstructuredList{} + sList.SetGroupVersionKind(schema.GroupVersionKind{ + Kind: "SecretList", + Version: "v1", + }) + err = a.DynamicClient.List(context.Background(), sList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(sList.Items), 2) + + // Run prune and assert there are no objects get deleted + pruneObject, err := kp.GetPruneConfig(f2) + assert.NoError(t, err) + p, donep, err := wiretest.InitializePrune(pruneObject, &object.Commit{}, buf) + defer donep() + assert.NoError(t, err) + p.DynamicClient = a.DynamicClient + pr, err := p.Do() + assert.NoError(t, err) + assert.Equal(t, len(pr.Resources), 0) + + // Confirm that there are two Secrets + err = a.DynamicClient.List(context.Background(), sList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(sList.Items), 2) +} + +func setupKustomizeWithStatefulSet(s string) (string, error) { + f, err := ioutil.TempDir("/tmp", "TestApply") + if err != nil { + return "", err + } + err = ioutil.WriteFile(filepath.Join(f, "kustomization.yaml"), []byte(`apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +`+s+ + ` +resources: +- statefulset.yaml + +inventory: + type: ConfigMap + configMap: + name: inventory + namespace: default + +namespace: default +`), 0644) + if err != nil { + return "", err + } + + err = ioutil.WriteFile(filepath.Join(f, "statefulset.yaml"), []byte(` +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: web +spec: + selector: + matchLabels: + app: nginx + serviceName: "nginx" + replicas: 3 # by default is 1 + template: + metadata: + labels: + app: nginx + spec: + terminationGracePeriodSeconds: 10 + containers: + - name: nginx + image: k8s.gcr.io/nginx-slim:0.8 + ports: + - containerPort: 80 + name: web + env: + - name: PASSWORD + valueFrom: + secretKeyRef: + name: pass + key: password +`), 0644) + if err != nil { + return "", err + } + + return f, nil +} + +/* TestPruneConfigMapWithStatefulSet take following steps + 1. create a Kustomization with a SecretGenerator, a StatefulSet + that refers to the generated Secret as well as an inventory object + 2. apply the kustomization + 3. update the SecretGenerator so that the Secret that gets generated + has a different name + 4. apply the kustomization again + 5. confirm that there are 2 Secrets + 6. run prune + 7. confirm that there are 2 Secrets, the first generated Secret is + not deleted since it is referred by the StatefulSet and the + Deployment object is not removed yet. +*/ +func TestPruneConfigMapWithStatefulSet(t *testing.T) { + buf := new(bytes.Buffer) + kp := wiretest.InitializConfigProvider() + + // setup the first version of resource configurations + // and run apply + f1, err := setupKustomizeWithStatefulSet(` +secretGenerator: +- name: pass + literals: + - password=secret +`) + assert.NoError(t, err) + defer os.RemoveAll(f1) + assert.NoError(t, err) + objects, err := kp.GetConfig(f1) + assert.NoError(t, err) + a, donea, err := wiretest.InitializeApply(objects, &object.Commit{}, buf) + assert.NoError(t, err) + defer donea() + _, err = a.Do() + assert.NoError(t, err) + + // setup the second version of resource configurations + // and run apply + f2, err := setupKustomizeWithStatefulSet(` +secretGenerator: +- name: pass + literals: + - password=secret + - a=b +`) + assert.NoError(t, err) + defer os.RemoveAll(f2) + a.Resources, err = kp.GetConfig(f2) + assert.NoError(t, err) + _, err = a.Do() + assert.NoError(t, err) + + // Confirm that there are two Secrets + sList := &unstructured.UnstructuredList{} + sList.SetGroupVersionKind(schema.GroupVersionKind{ + Kind: "SecretList", + Version: "v1", + }) + err = a.DynamicClient.List(context.Background(), sList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(sList.Items), 2) + + // Run prune and assert there are no objects get deleted + pruneObject, err := kp.GetPruneConfig(f2) + assert.NoError(t, err) + p, donep, err := wiretest.InitializePrune(pruneObject, &object.Commit{}, buf) + defer donep() + assert.NoError(t, err) + p.DynamicClient = a.DynamicClient + pr, err := p.Do() + assert.NoError(t, err) + assert.Equal(t, len(pr.Resources), 0) + + // Confirm that there are two Secrets + err = a.DynamicClient.List(context.Background(), sList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(sList.Items), 2) +} + +func setupKustomizeWithMultipleObjects(s string) (string, error) { + f, err := ioutil.TempDir("/tmp", "TestApply") + if err != nil { + return "", err + } + err = ioutil.WriteFile(filepath.Join(f, "kustomization.yaml"), []byte(`apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +`+s+ + ` +resources: +- deployment.yaml +- service.yaml + +inventory: + type: ConfigMap + configMap: + name: inventory + namespace: default + +namespace: default +`), 0644) + if err != nil { + return "", err + } + + err = ioutil.WriteFile(filepath.Join(f, "deployment.yaml"), []byte(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql + labels: + app: mysql +spec: + selector: + matchLabels: + app: mysql + strategy: + type: Recreate + template: + metadata: + labels: + app: mysql + spec: + containers: + - image: mysql:5.6 + name: mysql + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: pass + key: password +`), 0644) + if err != nil { + return "", err + } + + err = ioutil.WriteFile(filepath.Join(f, "service.yaml"), []byte(` +apiVersion: v1 +kind: Service +metadata: + name: mysql + labels: + app: mysql + annotations: {} +spec: + ports: + - port: 3306 + selector: + app: mysql +`), 0644) + if err != nil { + return "", err + } + + return f, nil +} + +/* TestPruneConfigMapWithMultipleObjects take following steps + 1. create a Kustomization with + a SecretGenerator + a Deployment that uses the generated Secret + a Service + an inventory ConfigMap + 2. apply the kustomization + 3. update the SecretGenerator so that the Secret that gets generated + has a different name + 3. add a namePrefix in the kustomization + 4. apply the kustomization again + 5. confirm that there are + 2 Secrets + 2 Deployments + 2 Services + 6. run prune and confirms 3 objects are deleted + 7. confirm that there are + 1 Secret + 1 Deployment + 1 Service +*/ +func TestPruneConfigMapWithMultipleObjects(t *testing.T) { + buf := new(bytes.Buffer) + kp := wiretest.InitializConfigProvider() + ctx := context.Background() + + // setup the first version of resource configurations + // and run apply + f1, err := setupKustomizeWithMultipleObjects(` +secretGenerator: +- name: pass + literals: + - password=secret +`) + assert.NoError(t, err) + defer os.RemoveAll(f1) + assert.NoError(t, err) + objects, err := kp.GetConfig(f1) + assert.NoError(t, err) + a, donea, err := wiretest.InitializeApply(objects, &object.Commit{}, buf) + assert.NoError(t, err) + defer donea() + + svList := &unstructured.UnstructuredList{} + svList.SetGroupVersionKind(schema.GroupVersionKind{ + Kind: "ServiceList", + Version: "v1", + }) + err = a.DynamicClient.List(ctx, svList, "default", nil) + assert.NoError(t, err) + serviceNumber := len(svList.Items) + + _, err = a.Do() + assert.NoError(t, err) + + // setup the second version of resource configurations + // and run apply + f2, err := setupKustomizeWithMultipleObjects(` +secretGenerator: +- name: pass + literals: + - password=secret + - a=b + +namePrefix: test- +`) + assert.NoError(t, err) + defer os.RemoveAll(f2) + a.Resources, err = kp.GetConfig(f2) + assert.NoError(t, err) + _, err = a.Do() + assert.NoError(t, err) + + // Confirm that there are two Secrets + sList := &unstructured.UnstructuredList{} + sList.SetGroupVersionKind(schema.GroupVersionKind{ + Kind: "SecretList", + Version: "v1", + }) + err = a.DynamicClient.List(ctx, sList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(sList.Items), 2) + + // Confirm that there are two Deployments + dpList := &unstructured.UnstructuredList{} + dpList.SetGroupVersionKind(schema.GroupVersionKind{ + Kind: "DeploymentList", + Version: "v1", + Group: "apps", + }) + err = a.DynamicClient.List(ctx, dpList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(dpList.Items), 2) + + // Confirm that there are two Services + err = a.DynamicClient.List(ctx, svList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(svList.Items), serviceNumber+2) + + // Run prune and assert there are 3 objects get deleted + pruneObject, err := kp.GetPruneConfig(f2) + assert.NoError(t, err) + p, donep, err := wiretest.InitializePrune(pruneObject, &object.Commit{}, buf) + defer donep() + assert.NoError(t, err) + p.DynamicClient = a.DynamicClient + pr, err := p.Do() + assert.NoError(t, err) + assert.Equal(t, len(pr.Resources), 3) + + // Confirm that there are one Secret + err = a.DynamicClient.List(ctx, sList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(sList.Items), 1) + + // Confirm that there are one Deployment + err = a.DynamicClient.List(ctx, dpList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(dpList.Items), 1) + + // Confirm that there are one Service + err = a.DynamicClient.List(ctx, svList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(svList.Items), serviceNumber+1) } diff --git a/internal/pkg/wirecli/wirecli.go b/internal/pkg/wirecli/wirecli.go index 412e25f..d16a0ad 100644 --- a/internal/pkg/wirecli/wirecli.go +++ b/internal/pkg/wirecli/wirecli.go @@ -26,6 +26,8 @@ import ( "sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wirek8s" ) +// TODO(Liujingfang1): split into per command wire + // ProviderSet defines dependencies for initializing objects var ProviderSet = wire.NewSet( wirek8s.ProviderSet,