From 26d681d6339dc50a5e90f70785ecb28a8e201750 Mon Sep 17 00:00:00 2001 From: Barni S Date: Mon, 3 Jun 2019 16:04:20 -0400 Subject: [PATCH] Adding status command --- cmd/apply/status/status.go | 15 +- go.mod | 7 +- go.sum | 17 +- internal/pkg/client/unstructured/helpers.go | 106 +++ .../pkg/client/unstructured/helpers_test.go | 118 ++++ internal/pkg/status/doc.go | 18 + internal/pkg/status/generic_status.go | 34 + internal/pkg/status/legacy_status.go | 238 +++++++ internal/pkg/status/status.go | 89 ++- internal/pkg/status/status_test.go | 650 +++++++++++++++++- internal/pkg/wirecli/wirestatus/wire_gen.go | 80 ++- internal/pkg/wirecli/wiretest/wire_gen.go | 20 +- pkg/cmd.go | 15 +- pkg/cmd_test.go | 12 +- pkg/wire_gen.go | 23 +- pkg/wirepkg.go | 3 + 16 files changed, 1352 insertions(+), 93 deletions(-) create mode 100644 internal/pkg/client/unstructured/helpers.go create mode 100644 internal/pkg/client/unstructured/helpers_test.go create mode 100644 internal/pkg/status/doc.go create mode 100644 internal/pkg/status/generic_status.go create mode 100644 internal/pkg/status/legacy_status.go diff --git a/cmd/apply/status/status.go b/cmd/apply/status/status.go index d118180..de4ae81 100644 --- a/cmd/apply/status/status.go +++ b/cmd/apply/status/status.go @@ -15,11 +15,11 @@ package status import ( "fmt" - - "sigs.k8s.io/cli-experimental/internal/pkg/util" + //"os" "github.com/spf13/cobra" "sigs.k8s.io/cli-experimental/internal/pkg/clik8s" + "sigs.k8s.io/cli-experimental/internal/pkg/util" "sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wirestatus" ) @@ -34,11 +34,18 @@ func GetApplyStatusCommand(a util.Args) *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) error { for i := range args { - r, err := wirestatus.DoStatus(clik8s.ResourceConfigPath(args[i]), cmd.OutOrStdout(), a) + result, err := wirestatus.DoStatus(clik8s.ResourceConfigPath(args[i]), cmd.OutOrStdout(), a) + for i := range result.Resources { + u := result.Resources[i].Resource + fmt.Fprintf(cmd.OutOrStdout(), "%s/%s %s", u.GetKind(), u.GetName(), result.Resources[i].Status) + if result.Resources[i].Error != nil { + fmt.Fprintf(cmd.OutOrStdout(), "(err: %s)", result.Resources[i].Error) + } + fmt.Fprintf(cmd.OutOrStdout(), "\n") + } if err != nil { return err } - fmt.Fprintf(cmd.OutOrStdout(), "Resources: %v\n", len(r.Resources)) } return nil } diff --git a/go.mod b/go.mod index 71a3b0d..6593e28 100644 --- a/go.mod +++ b/go.mod @@ -7,22 +7,21 @@ require ( github.com/Azure/go-autorest v11.7.0+incompatible // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/evanphx/json-patch v4.1.0+incompatible + github.com/ghodss/yaml v1.0.0 github.com/go-logr/logr v0.1.0 // indirect github.com/go-logr/zapr v0.1.1 // indirect github.com/go-openapi/spec v0.19.0 // indirect github.com/gogo/protobuf v1.2.1 // indirect github.com/golang/mock v1.2.0 // indirect github.com/google/btree v1.0.0 // indirect - github.com/google/go-cmp v0.3.0 // indirect github.com/google/wire v0.2.2-0.20190423202733-d079521b6f51 github.com/googleapis/gnostic v0.2.0 // indirect github.com/gophercloud/gophercloud v0.0.0-20190328150603-33e54f40ffcf // indirect github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect - github.com/grpc-ecosystem/grpc-gateway v1.8.5 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.6.2 // indirect github.com/imdario/mergo v0.3.7 // indirect github.com/json-iterator/go v1.1.6 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/modern-go/reflect2 v1.0.1 // indirect github.com/onsi/ginkgo v1.8.0 github.com/onsi/gomega v1.5.0 github.com/peterbourgon/diskv v2.0.1+incompatible // indirect @@ -45,7 +44,7 @@ require ( k8s.io/apiextensions-apiserver v0.0.0-20190328030136-8ada4fd07db4 k8s.io/apimachinery v0.0.0-20190326224424-4ceb6b6c5db5 k8s.io/client-go v11.0.0+incompatible - k8s.io/klog v0.2.0 // indirect + k8s.io/klog v0.3.2 // indirect k8s.io/kube-openapi v0.0.0-20190320154901-5e45bb682580 // indirect k8s.io/utils v0.0.0-20190308190857-21c4ce38f2a7 // indirect sigs.k8s.io/controller-runtime v0.1.10 diff --git a/go.sum b/go.sum index 0c93fd7..f4210a1 100644 --- a/go.sum +++ b/go.sum @@ -86,8 +86,6 @@ github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/wire v0.2.2-0.20190423202733-d079521b6f51 h1:oah2osnQk2JwZUU9BasNK201Ea2oNDY/3is4GYtnR7k= @@ -101,9 +99,8 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc h1:f8eY6cV/x1x+HLjOp4r72s/31/V2aTUtg5oKRRPf8/Q= github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.6.2 h1:8KyC64BiO8ndiGHY5DlFWWdangUPC9QHPakFRre/Ud0= github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/grpc-ecosystem/grpc-gateway v1.8.5 h1:2+KSC78XiO6Qy0hIjfc1OD9H+hsaJdJlb8Kqsd41CTE= -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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -141,9 +138,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 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 h1:ImOHKpmdLPXWX5KSYquUWXKaopEPuY7TPPUo18u9aOI= github.com/modern-go/reflect2 v0.0.0-20180228065516-1df9eeb2bb81/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= @@ -175,7 +171,6 @@ github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 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= @@ -225,7 +220,6 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -252,7 +246,6 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -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-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -303,7 +296,6 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/src-d/go-billy.v4 v4.2.1 h1:omN5CrMrMcQ+4I8bJ0wEhOBPanIRWzFC953IiXKdYzo= gopkg.in/src-d/go-billy.v4 v4.2.1/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= gopkg.in/src-d/go-git-fixtures.v3 v3.1.1 h1:XWW/s5W18RaJpmo1l0IYGqXKuJITWRFuA45iOf1dKJs= @@ -314,7 +306,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -334,8 +325,8 @@ k8s.io/client-go v7.0.0+incompatible h1:kiH+Y6hn+pc78QS/mtBfMJAMIIaWevHi++JvOGEE k8s.io/client-go v7.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o= k8s.io/client-go v11.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= -k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c= -k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.2 h1:qvP/U6CcZ6qyi/qSHlJKdlAboCzo3mT0DAm0XAarpz4= +k8s.io/klog v0.3.2/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/kube-openapi v0.0.0-20180510204742-b3f03f553288/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= k8s.io/kube-openapi v0.0.0-20190320154901-5e45bb682580 h1:fq0ZXW/BAIFZH+dazlups6JTVdwzRo5d9riFA103yuQ= k8s.io/kube-openapi v0.0.0-20190320154901-5e45bb682580/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= diff --git a/internal/pkg/client/unstructured/helpers.go b/internal/pkg/client/unstructured/helpers.go new file mode 100644 index 0000000..fbbf195 --- /dev/null +++ b/internal/pkg/client/unstructured/helpers.go @@ -0,0 +1,106 @@ +/* +Copyright 2015 The Kubernetes 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 unstructured + +import ( + "fmt" + api_unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "strings" +) + +func jsonPath(fields []string) string { + return "." + strings.Join(fields, ".") +} + +// NestedInt returns the int value of a nested field. +// Returns false if value is not found and an error if not an int +func NestedInt(obj map[string]interface{}, fields ...string) (int, bool, error) { + var i int + var i32 int32 + var i64 int64 + var ok bool + + val, found, err := api_unstructured.NestedFieldNoCopy(obj, fields...) + if !found || err != nil { + return 0, found, err + } + i, ok = val.(int) + if !ok { + i32, ok = val.(int32) + if ok { + i = int(i32) + } + } + if !ok { + i64, ok = val.(int64) + if ok { + i = int(i64) + } + } + if !ok { + return 0, true, fmt.Errorf("%v accessor error: %v is of the type %T, expected int", jsonPath(fields), val, val) + } + return i, true, nil +} + +// NestedMapSlice returns the value of a nested field. +// Returns false if value is not found and an error if not an slice of maps. +func NestedMapSlice(obj map[string]interface{}, fields ...string) ([]map[string]interface{}, bool, error) { + val, found, err := api_unstructured.NestedFieldNoCopy(obj, fields...) + if !found || err != nil { + return nil, found, err + } + array, ok := val.([]interface{}) + if !ok { + return nil, true, fmt.Errorf("%v accessor error: %v is of the type %T, expected []interface{}", jsonPath(fields), val, val) + } + + conditions := []map[string]interface{}{} + + for i := range array { + entry, ok := array[i].(map[string]interface{}) + if !ok { + return nil, true, fmt.Errorf("%v accessor error: %v[%d] is of the type %T, expected map[string]interface{}", jsonPath(fields), i, val, val) + } + conditions = append(conditions, entry) + + } + return conditions, true, nil +} + +// GetStringField - return field as string defaulting to value if not found +func GetStringField(obj map[string]interface{}, field, defaultValue string) string { + value := defaultValue + fieldV, ok := obj[field] + if ok { + stringV, ok := fieldV.(string) + if ok { + value = stringV + } + } + return value +} + +// GetConditions - return conditions array as []map[string]interface{} +func GetConditions(obj map[string]interface{}) []map[string]interface{} { + conditions, ok, err := NestedMapSlice(obj, "status", "conditions") + if err != nil { + fmt.Printf("err: %s", err) + return []map[string]interface{}{} + } + if !ok { + return []map[string]interface{}{} + } + return conditions +} diff --git a/internal/pkg/client/unstructured/helpers_test.go b/internal/pkg/client/unstructured/helpers_test.go new file mode 100644 index 0000000..47697ae --- /dev/null +++ b/internal/pkg/client/unstructured/helpers_test.go @@ -0,0 +1,118 @@ +/* +Copyright 2019 The Kubernetes 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 unstructured_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + helperu "sigs.k8s.io/cli-experimental/internal/pkg/client/unstructured" +) + +var emptyObj = map[string]interface{}{} +var testObj = map[string]interface{}{ + "f1": map[string]interface{}{ + "f1f2": map[string]interface{}{ + "f1f2i32": int32(32), + "f1f2i64": int64(64), + "f1f2float": 64.02, + "f1f2ms": []interface{}{ + map[string]interface{}{"f1f2ms0f1": 22}, + map[string]interface{}{"f1f2ms1f1": "index1"}, + }, + "f1f2msbad": []interface{}{ + map[string]interface{}{"f1f2ms0f1": 22}, + 32, + }, + }, + }, + "f2": map[string]interface{}{ + "f2f2": map[string]interface{}{}, + }, + + "ride": "dragon", + + "status": map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{"f1f2ms0f1": 22}, + map[string]interface{}{"f1f2ms1f1": "index1"}, + }, + }, +} + +func TestNestedInt(t *testing.T) { + v, found, err := helperu.NestedInt(testObj, "f1", "f1f2", "f1f2i32") + assert.NoError(t, err) + assert.Equal(t, found, true) + assert.Equal(t, int(32), v) + + v, found, err = helperu.NestedInt(testObj, "f1", "f1f2", "wrongname") + assert.NoError(t, err) + assert.Equal(t, found, false) + assert.Equal(t, int(0), v) + + v, found, err = helperu.NestedInt(testObj, "f1", "f1f2", "f1f2i64") + assert.NoError(t, err) + assert.Equal(t, found, true) + assert.Equal(t, int(64), v) + + v, found, err = helperu.NestedInt(testObj, "f1", "f1f2", "f1f2float") + assert.Error(t, err) + assert.Equal(t, found, true) + assert.Equal(t, int(0), v) +} + +func TestGetStringField(t *testing.T) { + v := helperu.GetStringField(testObj, "ride", "horse") + assert.Equal(t, v, "dragon") + + v = helperu.GetStringField(testObj, "destination", "north") + assert.Equal(t, v, "north") +} + +func TestNestedMapSlice(t *testing.T) { + v, found, err := helperu.NestedMapSlice(testObj, "f1", "f1f2", "f1f2ms") + assert.NoError(t, err) + assert.Equal(t, found, true) + assert.Equal(t, []map[string]interface{}{ + map[string]interface{}{"f1f2ms0f1": 22}, + map[string]interface{}{"f1f2ms1f1": "index1"}, + }, v) + + v, found, err = helperu.NestedMapSlice(testObj, "f1", "f1f2", "f1f2msbad") + assert.Error(t, err) + assert.Equal(t, found, true) + assert.Equal(t, []map[string]interface{}(nil), v) + + v, found, err = helperu.NestedMapSlice(testObj, "f1", "f1f2", "wrongname") + assert.NoError(t, err) + assert.Equal(t, found, false) + assert.Equal(t, []map[string]interface{}(nil), v) + + v, found, err = helperu.NestedMapSlice(testObj, "f1", "f1f2", "f1f2i64") + assert.Error(t, err) + assert.Equal(t, found, true) + assert.Equal(t, []map[string]interface{}(nil), v) +} + +func TestGetConditions(t *testing.T) { + v := helperu.GetConditions(emptyObj) + assert.Equal(t, []map[string]interface{}{}, v) + + v = helperu.GetConditions(testObj) + assert.Equal(t, []map[string]interface{}{ + map[string]interface{}{"f1f2ms0f1": 22}, + map[string]interface{}{"f1f2ms1f1": "index1"}, + }, v) +} diff --git a/internal/pkg/status/doc.go b/internal/pkg/status/doc.go new file mode 100644 index 0000000..a61c5b9 --- /dev/null +++ b/internal/pkg/status/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2019 The Kubernetes 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 status + +/* +Status +*/ diff --git a/internal/pkg/status/generic_status.go b/internal/pkg/status/generic_status.go new file mode 100644 index 0000000..d362a5f --- /dev/null +++ b/internal/pkg/status/generic_status.go @@ -0,0 +1,34 @@ +/* +Copyright 2019 The Kubernetes 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 status + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + clientu "sigs.k8s.io/cli-experimental/internal/pkg/client/unstructured" +) + +func readyConditionReader(u *unstructured.Unstructured) (bool, error) { + conditions := clientu.GetConditions(u.UnstructuredContent()) + for _, c := range conditions { + if clientu.GetStringField(c, "type", "") == "Ready" && clientu.GetStringField(c, "status", "") == "False" { + return false, nil + } + } + return true, nil +} + +// GetGenericReadyFn - True if we handle it as a known type +func GetGenericReadyFn(u *unstructured.Unstructured) IsReadyFn { + return readyConditionReader +} diff --git a/internal/pkg/status/legacy_status.go b/internal/pkg/status/legacy_status.go new file mode 100644 index 0000000..f28fb4a --- /dev/null +++ b/internal/pkg/status/legacy_status.go @@ -0,0 +1,238 @@ +/* +Copyright 2019 The Kubernetes 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 status + +import ( + "fmt" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + clientu "sigs.k8s.io/cli-experimental/internal/pkg/client/unstructured" +) + +// IsReadyFn - status getter +type IsReadyFn func(*unstructured.Unstructured) (bool, error) + +var legacyTypes = map[string]map[string]IsReadyFn{ + "": map[string]IsReadyFn{ + "Service": alwaysReady, + "Pod": podReady, + "PersistentVolumeClaim": pvcReady, + }, + "apps": map[string]IsReadyFn{ + "StatefulSet": stsReady, + "DaemonSet": daemonsetReady, + "Deployment": deploymentReady, + "ReplicaSet": replicasetReady, + }, + "policy": map[string]IsReadyFn{ + "PodDisruptionBudget": pdbReady, + }, + "batch": map[string]IsReadyFn{ + "CronJob": cronjobReady, + "Job": jobReady, + }, +} + +// GetLegacyReadyFn - True if we handle it as a known type +func GetLegacyReadyFn(u *unstructured.Unstructured) IsReadyFn { + gvk := u.GroupVersionKind() + g := gvk.Group + k := gvk.Kind + if _, ok := legacyTypes[g]; ok { + if fn, ok := legacyTypes[g][k]; ok { + return fn + } + } + return nil +} + +func alwaysReady(u *unstructured.Unstructured) (bool, error) { return true, nil } + +func compareIntFields(u *unstructured.Unstructured, field1, field2 []string, checkFuncs ...func(int, int) bool) (bool, error) { + v1, ok, err := clientu.NestedInt(u.UnstructuredContent(), field1...) + if err != nil { + return true, err + } + if !ok { + return false, fmt.Errorf("%v not found", field1) + } + + v2, ok, err := clientu.NestedInt(u.UnstructuredContent(), field2...) + if err != nil { + return true, err + } + if !ok { + return false, fmt.Errorf("%v not found", field2) + } + + rv := true + + for _, fn := range checkFuncs { + rv = rv && fn(v1, v2) + } + + return rv, nil +} + +func equalInt(v1, v2 int) bool { return v1 == v2 } +func geInt(v1, v2 int) bool { return v1 >= v2 } + +// Statefulset +func stsReady(u *unstructured.Unstructured) (bool, error) { + c1, err := compareIntFields(u, []string{"status", "readyReplicas"}, []string{"spec", "replicas"}, equalInt) + if err != nil { + return c1, err + } + c2, err := compareIntFields(u, []string{"status", "currentReplicas"}, []string{"spec", "replicas"}, equalInt) + if err != nil { + return c2, err + } + return c1 && c2, nil +} + +// Deployment +func deploymentReady(u *unstructured.Unstructured) (bool, error) { + progress := true + available := true + conditions := clientu.GetConditions(u.UnstructuredContent()) + + if len(conditions) == 0 { + return false, fmt.Errorf("no conditions in object") + } + + for _, c := range conditions { + switch clientu.GetStringField(c, "type", "") { + case "Progressing": //appsv1.DeploymentProgressing: + // https://github.com/kubernetes/kubernetes/blob/a3ccea9d8743f2ff82e41b6c2af6dc2c41dc7b10/pkg/controller/deployment/progress.go#L52 + status := clientu.GetStringField(c, "status", "") + reason := clientu.GetStringField(c, "reason", "") + if status != "True" || reason != "NewReplicaSetAvailable" { + progress = false + } + case "Available": //appsv1.DeploymentAvailable: + status := clientu.GetStringField(c, "status", "") + if status == "False" { + available = false + } + } + } + + return progress && available, nil +} + +// Replicaset +func replicasetReady(u *unstructured.Unstructured) (bool, error) { + failure := false + conditions := clientu.GetConditions(u.UnstructuredContent()) + + for _, c := range conditions { + switch clientu.GetStringField(c, "type", "") { + // https://github.com/kubernetes/kubernetes/blob/a3ccea9d8743f2ff82e41b6c2af6dc2c41dc7b10/pkg/controller/replicaset/replica_set_utils.go + case "ReplicaFailure": //appsv1.ReplicaSetReplicaFailure + status := clientu.GetStringField(c, "status", "") + if status == "True" { + failure = true + break + } + } + } + + c1, err := compareIntFields(u, []string{"status", "replicas"}, []string{"status", "readyReplicas"}, equalInt) + if err != nil { + return c1, err + } + c2, err := compareIntFields(u, []string{"status", "replicas"}, []string{"status", "availableReplicas"}, equalInt) + if err != nil { + return c2, err + } + + return !failure && c1 && c2, nil +} + +// Daemonset +func daemonsetReady(u *unstructured.Unstructured) (bool, error) { + c1, err := compareIntFields(u, []string{"status", "desiredNumberScheduled"}, []string{"status", "numberAvailable"}, equalInt) + if err != nil { + return c1, err + } + c2, err := compareIntFields(u, []string{"status", "desiredNumberScheduled"}, []string{"status", "numberReady"}, equalInt) + if err != nil { + return c2, err + } + + return c1 && c2, nil +} + +// PVC +func pvcReady(u *unstructured.Unstructured) (bool, error) { + val, found, err := unstructured.NestedString(u.UnstructuredContent(), "status", "phase") + if err != nil { + return false, err + } + if !found { + return false, fmt.Errorf(".status.phase not found") + } + return val == "Bound", nil // corev1.ClaimBound +} + +// Pod +func podReady(u *unstructured.Unstructured) (bool, error) { + conditions := clientu.GetConditions(u.UnstructuredContent()) + + for _, c := range conditions { + if clientu.GetStringField(c, "type", "") == "Ready" && (clientu.GetStringField(c, "status", "") == "True" || clientu.GetStringField(c, "reason", "") == "PodCompleted") { + return true, nil + } + } + return false, nil +} + +// PodDisruptionBudget +func pdbReady(u *unstructured.Unstructured) (bool, error) { + return compareIntFields(u, []string{"status", "currentHealthy"}, []string{"status", "desiredHealthy"}, geInt) +} + +// Cronjob +func cronjobReady(u *unstructured.Unstructured) (bool, error) { + obj := u.UnstructuredContent() + _, ok := obj["status"] + if !ok { + return false, nil + } + return true, nil +} + +// Job +func jobReady(u *unstructured.Unstructured) (bool, error) { + complete := false + failed := false + + conditions := clientu.GetConditions(u.UnstructuredContent()) + + // https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/job/utils.go#L24 + for _, c := range conditions { + status := clientu.GetStringField(c, "status", "") + switch clientu.GetStringField(c, "type", "") { + case "Complete": + if status == "True" { + complete = true + } + case "Failed": + if status == "True" { + failed = true + } + } + } + + return complete || failed, nil +} diff --git a/internal/pkg/status/status.go b/internal/pkg/status/status.go index 5600589..d5dae46 100644 --- a/internal/pkg/status/status.go +++ b/internal/pkg/status/status.go @@ -14,41 +14,92 @@ limitations under the License. package status import ( + "context" "fmt" "io" "gopkg.in/src-d/go-git.v4/plumbing/object" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/errors" + "sigs.k8s.io/cli-experimental/internal/pkg/client" "sigs.k8s.io/cli-experimental/internal/pkg/clik8s" ) // Status returns the status for rollouts type Status struct { + // DynamicClient is the client used to talk + // with the cluster + DynamicClient client.Client + // Out stores the output + Out io.Writer + // Resources is a list of resource configurations Resources clik8s.ResourceConfigs - Out io.Writer - Clientset *kubernetes.Clientset - Commit *object.Commit + // Commit is a git commit object + Commit *object.Commit +} + +// ResourceStatus - resource status +type ResourceStatus struct { + Resource *unstructured.Unstructured + Status string + Error error } // Result contains the Status Result type Result struct { - Resources clik8s.ResourceConfigs + Ready bool + Resources []ResourceStatus } -// Do executes the apply -func (s *Status) Do() (Result, error) { - fmt.Fprintf(s.Out, "Doing `cli-experimental apply status`\n") - if s.Commit != nil { - fmt.Fprintf(s.Out, "Commit %s\n", s.Commit.Hash.String()) - } - pods, err := s.Clientset.CoreV1().Pods("default").List(metav1.ListOptions{}) - if err != nil { - return Result{}, err - } - for _, p := range pods.Items { - fmt.Fprintf(s.Out, "Pod %s\n", p.Name) +// Do executes the status +func (a *Status) Do() (Result, error) { + ready := true + var errs []error + var rs = []ResourceStatus{} + + fmt.Fprintf(a.Out, "Doing `cli-experimental apply status`\n") + ctx := context.Background() + for _, u := range a.Resources { + err := a.DynamicClient.Get(ctx, + types.NamespacedName{Namespace: u.GetNamespace(), Name: u.GetName()}, u) + if err != nil { + rs = append(rs, ResourceStatus{Resource: u, Status: "GET_ERROR", Error: err}) + errs = append(errs, err) + continue + } + + // Ready indicator is a simple ANDing of all the individual resource readiness + uReady, err := IsReady(u) + if err != nil { + rs = append(rs, ResourceStatus{Resource: u, Status: "ERROR", Error: err}) + errs = append(errs, err) + continue + } + status := "Ready" + if !ready { + status = "InProgress" + } + rs = append(rs, ResourceStatus{Resource: u, Status: status, Error: nil}) + ready = ready && uReady } - return Result{Resources: s.Resources}, nil + if len(errs) != 0 { + return Result{Ready: ready, Resources: rs}, errors.NewAggregate(errs) + } + return Result{Ready: ready, Resources: rs}, nil +} + +// IsReady - return true if object is ready +func IsReady(u *unstructured.Unstructured) (bool, error) { + fn := GetLegacyReadyFn(u) + if fn == nil { + fn = GetGenericReadyFn(u) + } + + if fn != nil { + return fn(u) + } + + return true, nil } diff --git a/internal/pkg/status/status_test.go b/internal/pkg/status/status_test.go index 9da366f..d8ab652 100644 --- a/internal/pkg/status/status_test.go +++ b/internal/pkg/status/status_test.go @@ -17,19 +17,663 @@ import ( "bytes" "testing" + "github.com/ghodss/yaml" "github.com/stretchr/testify/assert" "gopkg.in/src-d/go-git.v4/plumbing/object" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/cli-experimental/internal/pkg/clik8s" "sigs.k8s.io/cli-experimental/internal/pkg/status" "sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wiretest" ) -func TestStatus(t *testing.T) { +func noitems() clik8s.ResourceConfigs { + return clik8s.ResourceConfigs(nil) +} + +func y2u(t *testing.T, spec string) *unstructured.Unstructured { + j, err := yaml.YAMLToJSON([]byte(spec)) + assert.NoError(t, err) + u, _, err := unstructured.UnstructuredJSONScheme.Decode(j, nil, nil) + assert.NoError(t, err) + return u.(*unstructured.Unstructured) +} + +func TestEmptyStatus(t *testing.T) { buf := new(bytes.Buffer) - a, done, err := wiretest.InitializeStatus(clik8s.ResourceConfigs(nil), &object.Commit{}, buf) + a, done, err := wiretest.InitializeStatus(noitems(), &object.Commit{}, buf) defer done() assert.NoError(t, err) r, err := a.Do() assert.NoError(t, err) - assert.Equal(t, status.Result{}, r) + assert.Equal(t, status.Result{Ready: true, Resources: []status.ResourceStatus{}}, r) +} + +var podNoStatus = ` +apiVersion: v1 +kind: Pod +metadata: + name: test +` +var podReady = ` +apiVersion: v1 +kind: Pod +metadata: + name: test + namespace: qual +status: + conditions: + - type: Ready + status: "True" +` + +var podCompletedOK = ` +apiVersion: v1 +kind: Pod +metadata: + name: test + namespace: qual +status: + phase: Succeeded + conditions: + - type: Ready + status: "False" + reason: PodCompleted + +` + +var podCompletedFail = ` +apiVersion: v1 +kind: Pod +metadata: + name: test + namespace: qual +status: + phase: Failed + conditions: + - type: Ready + status: "False" + reason: PodCompleted + +` + +// Test coverage using IsReady +func TestPodStatus(t *testing.T) { + r, err := status.IsReady(y2u(t, podNoStatus)) + assert.NoError(t, err) + assert.Equal(t, false, r) + + for _, spec := range []string{podReady, podCompletedOK, podCompletedFail} { + r, err = status.IsReady(y2u(t, spec)) + assert.NoError(t, err) + assert.Equal(t, true, r) + } +} + +var pvcNoStatus = ` +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: test +` +var pvcBound = ` +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: test + namespace: qual +status: + phase: Bound +` + +var pvcUnBound = ` +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: test + namespace: qual +status: + phase: UnBound +` + +func TestPVCStatus(t *testing.T) { + r, err := status.IsReady(y2u(t, pvcNoStatus)) + assert.Error(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, pvcBound)) + assert.NoError(t, err) + assert.Equal(t, true, r) + + r, err = status.IsReady(y2u(t, pvcUnBound)) + assert.NoError(t, err) + assert.Equal(t, false, r) +} + +var stsNoStatus = ` +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test +` +var stsBadStatus = ` +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test + namespace: qual +status: + currentReplicas: 1 +` + +var stsOK = ` +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test + namespace: qual +spec: + replicas: 4 +status: + currentReplicas: 4 + readyReplicas: 4 +` + +var stsLessReady = ` +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test + namespace: qual +spec: + replicas: 4 +status: + currentReplicas: 4 + readyReplicas: 2 +` +var stsLessCurrent = ` +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test + namespace: qual +spec: + replicas: 4 +status: + currentReplicas: 2 + readyReplicas: 4 +` + +func TestStsStatus(t *testing.T) { + r, err := status.IsReady(y2u(t, stsNoStatus)) + assert.Error(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, stsBadStatus)) + assert.Error(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, stsOK)) + assert.NoError(t, err) + assert.Equal(t, true, r) + + r, err = status.IsReady(y2u(t, stsLessReady)) + assert.NoError(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, stsLessCurrent)) + assert.NoError(t, err) + assert.Equal(t, false, r) +} + +var dsNoStatus = ` +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: test +` +var dsBadStatus = ` +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: test + namespace: qual +status: + currentReplicas: 1 +` + +var dsOK = ` +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: test + namespace: qual +status: + desiredNumberScheduled: 4 + numberAvailable: 4 + numberReady: 4 +` + +var dsLessReady = ` +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: test + namespace: qual +status: + desiredNumberScheduled: 4 + numberAvailable: 4 + numberReady: 2 +` +var dsLessAvailable = ` +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: test + namespace: qual +status: + desiredNumberScheduled: 4 + numberAvailable: 2 + numberReady: 4 +` + +func TestDaemonsetStatus(t *testing.T) { + r, err := status.IsReady(y2u(t, dsNoStatus)) + assert.Error(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, dsBadStatus)) + assert.Error(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, dsOK)) + assert.NoError(t, err) + assert.Equal(t, true, r) + + r, err = status.IsReady(y2u(t, dsLessReady)) + assert.NoError(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, dsLessAvailable)) + assert.NoError(t, err) + assert.Equal(t, false, r) +} + +var depNoStatus = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test +` + +var depOK = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test + namespace: qual +status: + conditions: + - type: Progressing + status: "True" + reason: NewReplicaSetAvailable + - type: Available + status: "True" +` + +var depNotProgressing = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test + namespace: qual +status: + conditions: + - type: Progressing + status: "False" + reason: Some reason + - type: Available + status: "True" +` + +var depNotAvailable = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test + namespace: qual +status: + conditions: + - type: Progressing + status: "True" + reason: NewReplicaSetAvailable + - type: Available + status: "False" +` + +func TestDeploymentStatus(t *testing.T) { + r, err := status.IsReady(y2u(t, depNoStatus)) + assert.Error(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, depOK)) + assert.NoError(t, err) + assert.Equal(t, true, r) + + r, err = status.IsReady(y2u(t, depNotProgressing)) + assert.NoError(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, depNotAvailable)) + assert.NoError(t, err) + assert.Equal(t, false, r) +} + +var rsNoStatus = ` +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: test +` + +var rsOK1 = ` +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: test + namespace: qual +status: + replicas: 2 + readyReplicas: 2 + availableReplicas: 2 + conditions: + - type: ReplicaFailure + status: "False" +` + +var rsOK2 = ` +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: test + namespace: qual +status: + replicas: 2 + readyReplicas: 2 + availableReplicas: 2 +` + +var rsLessReady = ` +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: test + namespace: qual +status: + replicas: 4 + readyReplicas: 2 + availableReplicas: 4 +` + +var rsLessAvailable = ` +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: test + namespace: qual +status: + replicas: 4 + readyReplicas: 4 + availableReplicas: 2 +` + +var rsReplicaFailure = ` +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: test + namespace: qual +status: + replicas: 4 + readyReplicas: 4 + availableReplicas: 4 + conditions: + - type: ReplicaFailure + status: "True" +` + +func TestReplicasetStatus(t *testing.T) { + r, err := status.IsReady(y2u(t, rsNoStatus)) + assert.Error(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, rsOK1)) + assert.NoError(t, err) + assert.Equal(t, true, r) + + r, err = status.IsReady(y2u(t, rsOK2)) + assert.NoError(t, err) + assert.Equal(t, true, r) + + r, err = status.IsReady(y2u(t, rsLessAvailable)) + assert.NoError(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, rsLessReady)) + assert.NoError(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, rsReplicaFailure)) + assert.NoError(t, err) + assert.Equal(t, false, r) +} + +var pdbNoStatus = ` +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: test +` + +var pdbOK1 = ` +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: test + namespace: qual +status: + currentHealthy: 2 + desiredHealthy: 2 +` + +var pdbMoreHealthy = ` +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: test + namespace: qual +status: + currentHealthy: 4 + desiredHealthy: 2 +` + +var pdbLessHealthy = ` +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: test + namespace: qual +status: + currentHealthy: 2 + desiredHealthy: 4 +` + +func TestPDBStatus(t *testing.T) { + r, err := status.IsReady(y2u(t, pdbNoStatus)) + assert.Error(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, pdbOK1)) + assert.NoError(t, err) + assert.Equal(t, true, r) + + r, err = status.IsReady(y2u(t, pdbMoreHealthy)) + assert.NoError(t, err) + assert.Equal(t, true, r) + + r, err = status.IsReady(y2u(t, pdbLessHealthy)) + assert.NoError(t, err) + assert.Equal(t, false, r) +} + +var crdNoStatus = ` +apiVersion: something/v1 +kind: MyCR +metadata: + name: test + namespace: qual +` + +var crdReady = ` +apiVersion: something/v1 +kind: MyCR +metadata: + name: test + namespace: qual +status: + conditions: + - type: Ready + status: "True" +` + +var crdNotReady = ` +apiVersion: something/v1 +kind: MyCR +metadata: + name: test + namespace: qual +status: + conditions: + - type: Ready + status: "False" +` + +var crdNoCondition = ` +apiVersion: something/v1 +kind: MyCR +metadata: + name: test + namespace: qual +status: + conditions: + - type: SomeCondition + status: "False" +` + +func TestCRDGenericStatus(t *testing.T) { + r, err := status.IsReady(y2u(t, crdNoStatus)) + assert.NoError(t, err) + assert.Equal(t, true, r) + + r, err = status.IsReady(y2u(t, crdReady)) + assert.NoError(t, err) + assert.Equal(t, true, r) + + r, err = status.IsReady(y2u(t, crdNotReady)) + assert.NoError(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, crdNoCondition)) + assert.NoError(t, err) + assert.Equal(t, true, r) +} + +var jobNoStatus = ` +apiVersion: batch/v1 +kind: Job +metadata: + name: test + namespace: qual +` + +var jobComplete = ` +apiVersion: batch/v1 +kind: Job +metadata: + name: test + namespace: qual +status: + conditions: + - type: Complete + status: "True" +` + +var jobFailed = ` +apiVersion: batch/v1 +kind: Job +metadata: + name: test + namespace: qual +status: + conditions: + - type: Failed + status: "True" +` + +var jobInProgress = ` +apiVersion: batch/v1 +kind: Job +metadata: + name: test + namespace: qual +status: + conditions: + - type: Failed + status: "False" + - type: Complete + status: "False" +` + +func TestJobStatus(t *testing.T) { + r, err := status.IsReady(y2u(t, jobNoStatus)) + assert.NoError(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, jobComplete)) + assert.NoError(t, err) + assert.Equal(t, true, r) + + r, err = status.IsReady(y2u(t, jobFailed)) + assert.NoError(t, err) + assert.Equal(t, true, r) + + r, err = status.IsReady(y2u(t, jobInProgress)) + assert.NoError(t, err) + assert.Equal(t, false, r) +} + +var cronjobNoStatus = ` +apiVersion: batch/v1 +kind: CronJob +metadata: + name: test + namespace: qual +` + +var cronjobWithStatus = ` +apiVersion: batch/v1 +kind: CronJob +metadata: + name: test + namespace: qual +status: +` + +func TestCronJobStatus(t *testing.T) { + r, err := status.IsReady(y2u(t, cronjobNoStatus)) + assert.NoError(t, err) + assert.Equal(t, false, r) + + r, err = status.IsReady(y2u(t, cronjobWithStatus)) + assert.NoError(t, err) + assert.Equal(t, true, r) } diff --git a/internal/pkg/wirecli/wirestatus/wire_gen.go b/internal/pkg/wirecli/wirestatus/wire_gen.go index 66aed0e..d78ce41 100644 --- a/internal/pkg/wirecli/wirestatus/wire_gen.go +++ b/internal/pkg/wirecli/wirestatus/wire_gen.go @@ -18,6 +18,26 @@ import ( // Injectors from wire.go: func InitializeStatus(resourceConfigPath clik8s.ResourceConfigPath, writer io.Writer, args util.Args) (*status.Status, error) { + configFlags, err := wirek8s.NewConfigFlags(args) + if err != nil { + return nil, err + } + config, err := wirek8s.NewRestConfig(configFlags) + if err != nil { + return nil, err + } + dynamicInterface, err := wirek8s.NewDynamicClient(config) + if err != nil { + return nil, err + } + restMapper, err := wirek8s.NewRestMapper(config) + if err != nil { + return nil, err + } + client, err := wirek8s.NewClient(dynamicInterface, restMapper) + if err != nil { + return nil, err + } pluginConfig := wireconfig.NewPluginConfig() factory := wireconfig.NewResMapFactory(pluginConfig) fileSystem := wireconfig.NewFileSystem() @@ -27,31 +47,39 @@ func InitializeStatus(resourceConfigPath clik8s.ResourceConfigPath, writer io.Wr if err != nil { return nil, err } - configFlags, err := wirek8s.NewConfigFlags(args) - if err != nil { - return nil, err - } - config, err := wirek8s.NewRestConfig(configFlags) - if err != nil { - return nil, err - } - clientset, err := wirek8s.NewKubernetesClientSet(config) - if err != nil { - return nil, err - } repository := wiregit.NewOptionalRepository(resourceConfigPath) commitIter := wiregit.NewOptionalCommitIter(repository) commit := wiregit.NewOptionalCommit(commitIter) statusStatus := &status.Status{ - Resources: resourceConfigs, - Out: writer, - Clientset: clientset, - Commit: commit, + DynamicClient: client, + Out: writer, + Resources: resourceConfigs, + Commit: commit, } return statusStatus, nil } func DoStatus(resourceConfigPath clik8s.ResourceConfigPath, writer io.Writer, args util.Args) (status.Result, error) { + configFlags, err := wirek8s.NewConfigFlags(args) + if err != nil { + return status.Result{}, err + } + config, err := wirek8s.NewRestConfig(configFlags) + if err != nil { + return status.Result{}, err + } + dynamicInterface, err := wirek8s.NewDynamicClient(config) + if err != nil { + return status.Result{}, err + } + restMapper, err := wirek8s.NewRestMapper(config) + if err != nil { + return status.Result{}, err + } + client, err := wirek8s.NewClient(dynamicInterface, restMapper) + if err != nil { + return status.Result{}, err + } pluginConfig := wireconfig.NewPluginConfig() factory := wireconfig.NewResMapFactory(pluginConfig) fileSystem := wireconfig.NewFileSystem() @@ -61,26 +89,14 @@ func DoStatus(resourceConfigPath clik8s.ResourceConfigPath, writer io.Writer, ar if err != nil { return status.Result{}, err } - configFlags, err := wirek8s.NewConfigFlags(args) - if err != nil { - return status.Result{}, err - } - config, err := wirek8s.NewRestConfig(configFlags) - if err != nil { - return status.Result{}, err - } - clientset, err := wirek8s.NewKubernetesClientSet(config) - if err != nil { - return status.Result{}, err - } repository := wiregit.NewOptionalRepository(resourceConfigPath) commitIter := wiregit.NewOptionalCommitIter(repository) commit := wiregit.NewOptionalCommit(commitIter) statusStatus := &status.Status{ - Resources: resourceConfigs, - Out: writer, - Clientset: clientset, - Commit: commit, + DynamicClient: client, + Out: writer, + Resources: resourceConfigs, + Commit: commit, } result, err := NewStatusCommandResult(statusStatus, writer) if err != nil { diff --git a/internal/pkg/wirecli/wiretest/wire_gen.go b/internal/pkg/wirecli/wiretest/wire_gen.go index 500e50c..edd8d35 100644 --- a/internal/pkg/wirecli/wiretest/wire_gen.go +++ b/internal/pkg/wirecli/wiretest/wire_gen.go @@ -33,16 +33,26 @@ func InitializeStatus(resourceConfigs clik8s.ResourceConfigs, commit *object.Com if err != nil { return nil, nil, err } - clientset, err := wirek8s.NewKubernetesClientSet(config) + dynamicInterface, err := wirek8s.NewDynamicClient(config) + if err != nil { + cleanup() + return nil, nil, err + } + restMapper, err := wirek8s.NewRestMapper(config) + if err != nil { + cleanup() + return nil, nil, err + } + client, err := wirek8s.NewClient(dynamicInterface, restMapper) if err != nil { cleanup() return nil, nil, err } statusStatus := &status.Status{ - Resources: resourceConfigs, - Out: writer, - Clientset: clientset, - Commit: commit, + DynamicClient: client, + Out: writer, + Resources: resourceConfigs, + Commit: commit, } return statusStatus, func() { cleanup() diff --git a/pkg/cmd.go b/pkg/cmd.go index b66ec41..fe63ba3 100644 --- a/pkg/cmd.go +++ b/pkg/cmd.go @@ -19,15 +19,17 @@ import ( "sigs.k8s.io/cli-experimental/internal/pkg/delete" "sigs.k8s.io/cli-experimental/internal/pkg/prune" "sigs.k8s.io/cli-experimental/internal/pkg/resourceconfig" + "sigs.k8s.io/cli-experimental/internal/pkg/status" ) // Cmd is a wrapper for different structs: // apply, prune and delete // These structs share the same client type Cmd struct { - Applier *apply.Apply - Pruner *prune.Prune - Deleter *delete.Delete + Applier *apply.Apply + Pruner *prune.Prune + Deleter *delete.Delete + StatusGetter *status.Status } // Apply applies resources given the input as a slice of unstructured resources @@ -54,3 +56,10 @@ func (a *Cmd) Delete(resources []*unstructured.Unstructured) error { _, err := a.Deleter.Do() return err } + +// Status returns the status given the input as a slice of unstructured resources +func (a *Cmd) Status(resources []*unstructured.Unstructured) error { + a.StatusGetter.Resources = resources + _, err := a.StatusGetter.Do() + return err +} diff --git a/pkg/cmd_test.go b/pkg/cmd_test.go index 630907c..ea03dd2 100644 --- a/pkg/cmd_test.go +++ b/pkg/cmd_test.go @@ -61,8 +61,7 @@ func setupResourcesV1() []*unstructured.Unstructured { r2.SetName("inventory") r2.SetNamespace("default") r2.SetAnnotations(map[string]string{ - inventory.InventoryAnnotation: - "{\"current\":{\"~G_v1_ConfigMap|default|cm1\":null}}", + inventory.InventoryAnnotation: "{\"current\":{\"~G_v1_ConfigMap|default|cm1\":null}}", inventory.InventoryHashAnnotation: "1234567", }) return []*unstructured.Unstructured{r1, r2} @@ -88,8 +87,7 @@ func setupResourcesV2() []*unstructured.Unstructured { r2.SetName("inventory") r2.SetNamespace("default") r2.SetAnnotations(map[string]string{ - inventory.InventoryAnnotation: - "{\"current\":{\"~G_v1_ConfigMap|default|cm2\":null}}", + inventory.InventoryAnnotation: "{\"current\":{\"~G_v1_ConfigMap|default|cm2\":null}}", inventory.InventoryHashAnnotation: "7654321", }) return []*unstructured.Unstructured{r1, r2} @@ -157,6 +155,12 @@ func TestCmd(t *testing.T) { assert.NoError(t, err) assert.Equal(t, len(cmList.Items), 2) + err = cmd.Status(resources) + assert.NoError(t, err) + err = c.List(context.Background(), cmList, "default", nil) + assert.NoError(t, err) + assert.Equal(t, len(cmList.Items), 2) + err = cmd.Delete(resources) assert.NoError(t, err) err = c.List(context.Background(), cmList, "default", nil) diff --git a/pkg/wire_gen.go b/pkg/wire_gen.go index 101c0da..da6be67 100644 --- a/pkg/wire_gen.go +++ b/pkg/wire_gen.go @@ -10,6 +10,7 @@ import ( "sigs.k8s.io/cli-experimental/internal/pkg/apply" delete2 "sigs.k8s.io/cli-experimental/internal/pkg/delete" "sigs.k8s.io/cli-experimental/internal/pkg/prune" + "sigs.k8s.io/cli-experimental/internal/pkg/status" "sigs.k8s.io/cli-experimental/internal/pkg/util" "sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wirek8s" "sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wiretest" @@ -50,10 +51,15 @@ func InitializeCmd(writer io.Writer, args util.Args) (*Cmd, error) { DynamicClient: client, Out: writer, } + statusStatus := &status.Status{ + DynamicClient: client, + Out: writer, + } cmd := &Cmd{ - Applier: applyApply, - Pruner: prunePrune, - Deleter: deleteDelete, + Applier: applyApply, + Pruner: prunePrune, + Deleter: deleteDelete, + StatusGetter: statusStatus, } return cmd, nil } @@ -90,10 +96,15 @@ func InitializeFakeCmd(writer io.Writer, args util.Args) (*Cmd, func(), error) { DynamicClient: client, Out: writer, } + statusStatus := &status.Status{ + DynamicClient: client, + Out: writer, + } cmd := &Cmd{ - Applier: applyApply, - Pruner: prunePrune, - Deleter: deleteDelete, + Applier: applyApply, + Pruner: prunePrune, + Deleter: deleteDelete, + StatusGetter: statusStatus, } return cmd, func() { cleanup() diff --git a/pkg/wirepkg.go b/pkg/wirepkg.go index 868ca75..1c187a4 100644 --- a/pkg/wirepkg.go +++ b/pkg/wirepkg.go @@ -18,6 +18,7 @@ import ( "sigs.k8s.io/cli-experimental/internal/pkg/apply" "sigs.k8s.io/cli-experimental/internal/pkg/delete" "sigs.k8s.io/cli-experimental/internal/pkg/prune" + "sigs.k8s.io/cli-experimental/internal/pkg/status" "sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wirek8s" "sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wiretest" ) @@ -27,6 +28,7 @@ var ProviderSet = wire.NewSet( wire.Struct(new(apply.Apply), "DynamicClient", "Out"), wire.Struct(new(prune.Prune), "DynamicClient", "Out"), wire.Struct(new(delete.Delete), "DynamicClient", "Out"), + wire.Struct(new(status.Status), "DynamicClient", "Out"), wire.Struct(new(Cmd), "*"), wirek8s.ProviderSet, ) @@ -36,6 +38,7 @@ var ProviderSetForTesting = wire.NewSet( wiretest.NewRestConfig, wirek8s.NewClient, wirek8s.NewRestMapper, wire.Struct(new(apply.Apply), "DynamicClient", "Out"), wire.Struct(new(prune.Prune), "DynamicClient", "Out"), + wire.Struct(new(status.Status), "DynamicClient", "Out"), wire.Struct(new(delete.Delete), "DynamicClient", "Out"), wire.Struct(new(Cmd), "*"), )