Merge pull request #1983 from mortent/statusCmdTests

Add tests for status cli
This commit is contained in:
Jeff Regan
2020-01-07 09:56:04 -08:00
committed by GitHub
14 changed files with 1019 additions and 24 deletions

View File

@@ -15,7 +15,7 @@ linters:
- dogsled
- dupl
- errcheck
- funlen
# - funlen
# - gochecknoinits
- goconst
- gocritic

View File

@@ -3,9 +3,12 @@ module sigs.k8s.io/kustomize/cmd/resource
go 1.12
require (
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/pkg/errors v0.8.1
github.com/spf13/cobra v0.0.5
github.com/stretchr/testify v1.4.0
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 // indirect
k8s.io/api v0.0.0-20190918155943-95b840bb6a1f
k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655
k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90
sigs.k8s.io/controller-runtime v0.4.0

View File

@@ -18,6 +18,8 @@ github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
@@ -349,8 +351,10 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -16,7 +16,9 @@ import (
// GetEventsRunner returns a command EventsRunner.
func GetEventsRunner() *EventsRunner {
r := &EventsRunner{}
r := &EventsRunner{
createClientFunc: createClient,
}
c := &cobra.Command{
Use: "events DIR...",
Short: commands.EventsShort,
@@ -46,13 +48,15 @@ type EventsRunner struct {
Interval time.Duration
Timeout time.Duration
Command *cobra.Command
createClientFunc createClientFunc
}
func (r *EventsRunner) runE(c *cobra.Command, args []string) error {
ctx := context.Background()
// Create a client and use it to set up a new resolver.
client, err := getClient()
client, err := r.createClientFunc()
if err != nil {
return errors.Wrap(err, "error creating client")
}

View File

@@ -0,0 +1,298 @@
package cmd
import (
"bytes"
"fmt"
"reflect"
"regexp"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kstatus/status"
"sigs.k8s.io/kustomize/kstatus/wait"
)
func TestEventsNoResources(t *testing.T) {
inBuffer := &bytes.Buffer{}
outBuffer := &bytes.Buffer{}
fakeClient := &FakeClient{}
r := GetEventsRunner()
r.createClientFunc = newClientFunc(fakeClient)
r.Command.SetArgs([]string{})
r.Command.SetIn(inBuffer)
r.Command.SetOut(outBuffer)
err := r.Command.Execute()
if !assert.NoError(t, err) {
return
}
eventOutput := parseEventOutput(t, outBuffer.String())
if want, got := 1, len(eventOutput.events); want != got {
t.Errorf("expected %d events, but got %d", want, got)
}
event := eventOutput.events[0]
if want, got := status.CurrentStatus, event.aggStatus; want != got {
t.Errorf("expected agg status %s, but got %s", want, got)
}
}
func TestEventsMultipleUpdates(t *testing.T) {
inBuffer := &bytes.Buffer{}
outBuffer := &bytes.Buffer{}
_, err := fmt.Fprint(inBuffer, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: bar
namespace: default
`)
if !assert.NoError(t, err) {
return
}
fakeClient := &FakeClient{
resourceCallbackMap: map[string]ResourceGetCallback{
"Deployment": createDeploymentStatusFunc(),
},
}
r := GetEventsRunner()
r.createClientFunc = newClientFunc(fakeClient)
r.Command.SetArgs([]string{})
r.Command.SetIn(inBuffer)
r.Command.SetOut(outBuffer)
err = r.Command.Execute()
if !assert.NoError(t, err) {
return
}
eventOutput := parseEventOutput(t, outBuffer.String())
aggStatuses := eventOutput.allAggStatuses()
expectedAggStatuses := []status.Status{
status.InProgressStatus,
status.InProgressStatus,
status.InProgressStatus,
status.InProgressStatus,
status.CurrentStatus,
status.CurrentStatus,
}
if !reflect.DeepEqual(aggStatuses, expectedAggStatuses) {
t.Errorf("expected agg statuses to be %s, but got %s", joinStatuses(expectedAggStatuses),
joinStatuses(aggStatuses))
}
resources := eventOutput.allResources()
if want, got := 1, len(resources); want != got {
t.Errorf("expected %d resource, but got %d", want, got)
}
resource := resources[0]
resourceStatuses := eventOutput.statusesForResource(resource)
expectedResourceStatuses := []status.Status{
status.InProgressStatus,
status.InProgressStatus,
status.InProgressStatus,
status.InProgressStatus,
status.CurrentStatus,
}
if !reflect.DeepEqual(resourceStatuses, expectedResourceStatuses) {
t.Errorf("expected statuses to be %s, but got %s", joinStatuses(expectedResourceStatuses),
joinStatuses(resourceStatuses))
}
}
func TestEventsMultipleResources(t *testing.T) {
inBuffer := &bytes.Buffer{}
outBuffer := &bytes.Buffer{}
_, err := fmt.Fprint(inBuffer, `
apiVersion: v1
kind: List
items:
- apiVersion: v1
kind: Pod
metadata:
name: foo
namespace: default
- apiVersion: v1
kind: Service
metadata:
name: bar
namespace: default
`)
if !assert.NoError(t, err) {
return
}
fakeClient := &FakeClient{
resourceCallbackMap: map[string]ResourceGetCallback{
"Pod": createPodStatusFunc(),
"Service": createServiceStatusFunc(),
},
}
r := GetEventsRunner()
r.createClientFunc = newClientFunc(fakeClient)
r.Command.SetArgs([]string{})
r.Command.SetIn(inBuffer)
r.Command.SetOut(outBuffer)
err = r.Command.Execute()
if !assert.NoError(t, err) {
return
}
eventOutput := parseEventOutput(t, outBuffer.String())
aggStatuses := eventOutput.allAggStatuses()
expectedAggStatuses := []status.Status{
status.UnknownStatus,
status.CurrentStatus,
status.CurrentStatus,
}
if !reflect.DeepEqual(aggStatuses, expectedAggStatuses) {
t.Errorf("expected agg statuses to be %s, but got %s", joinStatuses(expectedAggStatuses),
joinStatuses(aggStatuses))
}
resources := eventOutput.allResources()
if want, got := 2, len(resources); got != want {
t.Errorf("expected %d resource, but got %d", want, got)
}
for _, resource := range resources {
resourceStatuses := eventOutput.statusesForResource(resource)
if want, got := status.CurrentStatus, resourceStatuses[len(resourceStatuses)-1]; want != got {
t.Errorf("expected resource %q to have final status %s, but got %s", resource.name, want, got)
}
}
}
type EventOutput struct {
events []EventOutputLine
unknownLines []string
}
func (e *EventOutput) allAggStatuses() []status.Status {
var aggStatuses []status.Status
for _, event := range e.events {
aggStatuses = append(aggStatuses, event.aggStatus)
}
return aggStatuses
}
func (e *EventOutput) allResources() []ResourceIdentifier {
var resources []ResourceIdentifier
seenResources := make(map[ResourceIdentifier]bool)
for _, event := range e.events {
if !event.isResourceUpdateEvent() {
continue
}
r := event.identifier
if _, found := seenResources[r]; !found {
resources = append(resources, r)
seenResources[r] = true
}
}
return resources
}
func (e *EventOutput) statusesForResource(resource ResourceIdentifier) []status.Status {
var statuses []status.Status
for _, event := range e.events {
if !event.isResourceUpdateEvent() {
continue
}
if event.identifier.Equals(resource) {
statuses = append(statuses, event.status)
}
}
return statuses
}
type EventOutputLine struct {
eventType string
aggStatus status.Status
identifier ResourceIdentifier
status status.Status
message string
}
func (e *EventOutputLine) isResourceUpdateEvent() bool {
return e.eventType == string(wait.ResourceUpdate)
}
var (
eventRegex = regexp.MustCompile(`^\s*` +
`(?P<eventType>\S+)\s+` +
`(?P<aggStatus>\S+)\s+` +
`((?P<resourceType>\S+)\s+` +
`(?P<namespace>\S+)\s+` +
`(?P<name>\S+)\s+` +
`(?P<status>\S+)\s+` +
`(?P<message>.*\S)){0,1}` +
`\s*$`)
eventHeaderRegex = regexp.MustCompile(`^\s*` +
`EVENT TYPE\s+` +
`AGG STATUS\s+` +
`TYPE\s+` +
`NAMESPACE\s+` +
`NAME\s+` +
`STATUS\s+` +
`MESSAGE` +
`\s*$`)
)
func parseEventOutput(_ *testing.T, output string) EventOutput {
var eventOutput EventOutput
lines := strings.Split(output, "\n")
for _, line := range lines {
if len(line) == 0 {
continue // Ignore empty lines
}
match := eventHeaderRegex.FindStringSubmatch(line)
if match != nil {
continue // Ignore headers
}
match = eventRegex.FindStringSubmatch(line)
if match == nil {
eventOutput.unknownLines = append(eventOutput.unknownLines, line)
continue
}
eventOutputLine := EventOutputLine{
eventType: match[1],
aggStatus: status.FromStringOrDie(match[2]),
}
if eventOutputLine.eventType == string(wait.ResourceUpdate) {
resourceType := match[4]
parts := strings.Split(resourceType, "/")
var identifier ResourceIdentifier
if len(parts) == 2 {
identifier.apiVersion = parts[0]
identifier.kind = parts[1]
} else {
identifier.apiVersion = strings.Join(parts[:2], "/")
identifier.kind = parts[2]
}
identifier.namespace = match[5]
identifier.name = match[6]
eventOutputLine.identifier = identifier
eventOutputLine.status = status.FromStringOrDie(match[7])
eventOutputLine.message = match[8]
}
eventOutput.events = append(eventOutput.events, eventOutputLine)
}
return eventOutput
}

View File

@@ -17,7 +17,9 @@ import (
// GetFetchRunner returns a command FetchRunner.
func GetFetchRunner() *FetchRunner {
r := &FetchRunner{}
r := &FetchRunner{
createClientFunc: createClient,
}
c := &cobra.Command{
Use: "fetch DIR...",
Short: commands.FetchShort,
@@ -41,18 +43,20 @@ func FetchCommand() *cobra.Command {
type FetchRunner struct {
IncludeSubpackages bool
Command *cobra.Command
createClientFunc createClientFunc
}
func (r *FetchRunner) runE(c *cobra.Command, args []string) error {
ctx := context.Background()
// Create a new client and use it to set up a resolver.
client, err := getClient()
k8sClient, err := r.createClientFunc()
if err != nil {
return errors.Wrap(err, "error creating client")
return errors.Wrap(err, "error creating k8sClient")
}
resolver := wait.NewResolver(client, time.Minute)
resolver := wait.NewResolver(k8sClient, time.Minute)
// Set up a CaptureIdentifierFilter and run all inputs through the
// filter with the pipeline to capture the inventory of resources

View File

@@ -0,0 +1,230 @@
package cmd
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/acarl005/stripansi"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/kustomize/kstatus/status"
)
func TestEmptyManifest(t *testing.T) {
inBuffer := &bytes.Buffer{}
outBuffer := &bytes.Buffer{}
fakeClient := fake.NewFakeClientWithScheme(scheme)
r := GetFetchRunner()
r.createClientFunc = newClientFunc(fakeClient)
r.Command.SetArgs([]string{})
r.Command.SetIn(inBuffer)
r.Command.SetOut(outBuffer)
err := r.Command.Execute()
if !assert.NoError(t, err) {
return
}
output := outBuffer.String()
lines := strings.Split(output, "\n")
if want, got := 2, len(lines); want != got {
t.Errorf("Expected %d lines, but got %d", want, got)
}
}
func TestFetchStatusFromManifestStdIn(t *testing.T) {
inBuffer := &bytes.Buffer{}
outBuffer := &bytes.Buffer{}
_, err := fmt.Fprint(inBuffer, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: bar
namespace: default
`)
if !assert.NoError(t, err) {
return
}
deployment := createDeployment("bar", "default", 42, appsv1.DeploymentStatus{
ObservedGeneration: 1,
})
fakeClient := fake.NewFakeClientWithScheme(scheme, deployment)
r := GetFetchRunner()
r.createClientFunc = newClientFunc(fakeClient)
r.Command.SetArgs([]string{})
r.Command.SetIn(inBuffer)
r.Command.SetOut(outBuffer)
err = r.Command.Execute()
if !assert.NoError(t, err) {
return
}
cleanOutput := stripansi.Strip(outBuffer.String())
tableOutput := parseTableOutput(t, cleanOutput)
expectedResource := ResourceIdentifier{
apiVersion: "apps/v1",
kind: "Deployment",
namespace: "default",
name: "bar",
}
expectedStatus := status.InProgressStatus
expectedMessage := "Deployment generation is 2, but latest observed generation is 1"
verifyOutputContains(t, tableOutput, expectedResource, expectedStatus, expectedMessage)
}
//nolint:funlen
func TestFetchStatusFromManifestsFiles(t *testing.T) {
d, err := ioutil.TempDir("", "status-fetch-test")
if !assert.NoError(t, err) {
return
}
defer os.RemoveAll(d)
err = ioutil.WriteFile(filepath.Join(d, "dep.yaml"), []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
namespace: default
`), 0600)
if !assert.NoError(t, err) {
return
}
err = ioutil.WriteFile(filepath.Join(d, "svc.yaml"), []byte(`
apiVersion: v1
kind: Service
metadata:
name: foo
namespace: default
`), 0600)
if !assert.NoError(t, err) {
return
}
replicas := int32(42)
deployment := createDeployment("foo", "default", replicas, appsv1.DeploymentStatus{
ObservedGeneration: 2,
Replicas: replicas,
ReadyReplicas: replicas,
AvailableReplicas: replicas,
UpdatedReplicas: replicas,
Conditions: []appsv1.DeploymentCondition{
{
Type: appsv1.DeploymentAvailable,
Status: v1.ConditionTrue,
},
},
})
service := createService("foo", "default")
fakeClient := fake.NewFakeClientWithScheme(scheme, deployment, service)
outBuffer := &bytes.Buffer{}
r := GetFetchRunner()
r.createClientFunc = newClientFunc(fakeClient)
r.Command.SetArgs([]string{d})
r.Command.SetOut(outBuffer)
err = r.Command.Execute()
if !assert.NoError(t, err) {
return
}
cleanOutput := stripansi.Strip(outBuffer.String())
tableOutput := parseTableOutput(t, cleanOutput)
expectedDeploymentResource := ResourceIdentifier{
apiVersion: "apps/v1",
kind: "Deployment",
namespace: "default",
name: "foo",
}
expectedDeploymentStatus := status.CurrentStatus
expectedDeploymentMessage := "Deployment is available. Replicas: 42"
verifyOutputContains(t, tableOutput, expectedDeploymentResource, expectedDeploymentStatus, expectedDeploymentMessage)
expectedServiceResource := ResourceIdentifier{
apiVersion: "v1",
kind: "Service",
namespace: "default",
name: "foo",
}
expectedServiceStatus := status.CurrentStatus
expectedServiceMessage := "Service is ready"
verifyOutputContains(t, tableOutput, expectedServiceResource, expectedServiceStatus, expectedServiceMessage)
}
func createDeployment(name, namespace string, replicas int32, status appsv1.DeploymentStatus) *appsv1.Deployment {
return &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
APIVersion: "apps/v1",
Kind: "Deployment",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Generation: 2,
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
},
Status: status,
}
}
func createService(name, namespace string) *v1.Service {
return &v1.Service{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Service",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
}
}
func verifyOutputContains(t *testing.T, tableOutput TableOutput, resource ResourceIdentifier, status status.Status, message string) {
if len(tableOutput.Frames) == 0 {
t.Fatalf("expected match for resource %s, but output had no frames", resource.name)
}
firstFrame := tableOutput.Frames[0]
var foundResource ResourceOutput
match := false
for _, resourceOutput := range firstFrame.Resources {
if resourceOutput.identifier.Equals(resource) {
foundResource = resourceOutput
match = true
break
}
}
if !match {
t.Errorf("expected match for resource %s, but didn't find it", resource.name)
}
if want, got := status, foundResource.status; want != got {
t.Errorf("expected status %s for resource %s, but got %s", want, resource.name, got)
}
if want, got := message, foundResource.message; !strings.HasPrefix(want, got) {
t.Errorf("expected message %s for resource %s, but got %s", want, resource.name, got)
}
}

View File

@@ -0,0 +1,245 @@
package cmd
import (
"context"
"fmt"
"regexp"
"strings"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/kustomize/kstatus/status"
)
type TableOutput struct {
Frames []TableOutputFrame
UnknownRows []string
}
func (t *TableOutput) allAggStatuses() []status.Status {
var statuses []status.Status
for _, frame := range t.Frames {
if frame.AggregateStatus != "" {
statuses = append(statuses, frame.AggregateStatus)
}
}
return statuses
}
func (t *TableOutput) dedupedAggStatuses() []status.Status {
var dedupedStatuses []status.Status
statuses := t.allAggStatuses()
var previousStatus status.Status
for _, s := range statuses {
if s != previousStatus {
dedupedStatuses = append(dedupedStatuses, s)
previousStatus = s
}
}
return dedupedStatuses
}
func (t *TableOutput) resources() []ResourceIdentifier {
seenResources := make(map[ResourceIdentifier]bool)
var resources []ResourceIdentifier
for _, frame := range t.Frames {
for _, resource := range frame.Resources {
r := resource.identifier
_, found := seenResources[r]
if !found {
seenResources[r] = true
resources = append(resources, r)
}
}
}
return resources
}
func (t *TableOutput) dedupedStatusesForResource(resource ResourceIdentifier) []status.Status {
var dedupedStatuses []status.Status
var previousStatus status.Status
for _, frame := range t.Frames {
for _, r := range frame.Resources {
if r.identifier.Equals(resource) {
if r.status != previousStatus {
previousStatus = r.status
dedupedStatuses = append(dedupedStatuses, r.status)
}
}
}
}
return dedupedStatuses
}
type TableOutputFrame struct {
AggregateStatus status.Status
Resources []ResourceOutput
}
type ResourceIdentifier struct {
apiVersion string
kind string
name string
namespace string
}
func (r ResourceIdentifier) Equals(identifier ResourceIdentifier) bool {
return r.apiVersion == identifier.apiVersion &&
r.kind == identifier.kind &&
r.namespace == identifier.namespace &&
r.name == identifier.name
}
type ResourceOutput struct {
identifier ResourceIdentifier
status status.Status
message string
}
var (
headerRegex = regexp.MustCompile(`^\s*TYPE\s+NAMESPACE\s+NAME\s+STATUS\s+MESSAGE\s*$`)
resourceRegex = regexp.MustCompile(`^(?P<resourceType>\S+)\s+(?P<namespace>\S+)\s+(?P<name>\S+)\s+(?P<status>\S+)\s+(?P<message>.*\S)\s*$`)
aggStatusRegex = regexp.MustCompile(`^\s*AggregateStatus: (?P<aggregateStatus>\S+)\s*$`)
)
func parseTableOutput(_ *testing.T, output string) TableOutput {
tableOutput := TableOutput{}
lines := strings.Split(output, "\n")
hasAggStatus := false
var currentFrame TableOutputFrame
for i, line := range lines {
if len(line) == 0 {
continue // We don't care about empty lines.
}
// Check for lines with aggregate status. They are not always present, but if they are,
// they always start a new frame of output.
match := aggStatusRegex.FindStringSubmatch(line)
if match != nil {
hasAggStatus = true
if i != 0 {
tableOutput.Frames = append(tableOutput.Frames, currentFrame)
}
currentFrame = TableOutputFrame{
AggregateStatus: status.FromStringOrDie(match[1]),
}
continue
}
match = headerRegex.FindStringSubmatch(line)
if match != nil {
if !hasAggStatus {
if i != 0 {
tableOutput.Frames = append(tableOutput.Frames, currentFrame)
}
currentFrame = TableOutputFrame{}
}
continue
}
match = resourceRegex.FindStringSubmatch(line)
if match != nil {
var identifier ResourceIdentifier
resourceType := match[1]
parts := strings.Split(resourceType, "/")
if len(parts) == 2 {
identifier.apiVersion = parts[0]
identifier.kind = parts[1]
} else {
identifier.apiVersion = strings.Join(parts[:2], "/")
identifier.kind = parts[2]
}
identifier.namespace = match[2]
identifier.name = match[3]
res := ResourceOutput{
identifier: identifier,
}
res.status = status.FromStringOrDie(match[4])
res.message = match[5]
currentFrame.Resources = append(currentFrame.Resources, res)
continue
}
tableOutput.UnknownRows = append(tableOutput.UnknownRows, line)
}
tableOutput.Frames = append(tableOutput.Frames, currentFrame)
return tableOutput
}
func createDeploymentStatusFunc() func(*unstructured.Unstructured) error {
metadataMap := map[string]interface{}{
"generation": int64(2),
}
specMap := map[string]interface{}{
"replicas": int64(2),
}
statusMap := map[string]interface{}{
"observedGeneration": int64(2),
"replicas": int64(4),
"updatedReplicas": int64(4),
"readyReplicas": int64(4),
}
var conditions = make([]interface{}, 0)
conditions = append(conditions, map[string]interface{}{
"type": "Available",
"status": "True",
})
callbackCount := int64(0)
return func(deployment *unstructured.Unstructured) error {
_ = unstructured.SetNestedMap(deployment.Object, metadataMap, "metadata")
_ = unstructured.SetNestedMap(deployment.Object, specMap, "spec")
statusMap["availableReplicas"] = callbackCount
_ = unstructured.SetNestedMap(deployment.Object, statusMap, "status")
_ = unstructured.SetNestedSlice(deployment.Object, conditions, "status", "conditions")
callbackCount++
return nil
}
}
func createServiceStatusFunc() func(*unstructured.Unstructured) error {
return func(*unstructured.Unstructured) error {
return nil
}
}
func createPodStatusFunc() func(*unstructured.Unstructured) error {
statusMap := map[string]interface{}{
"phase": "Succeeded",
}
return func(pod *unstructured.Unstructured) error {
_ = unstructured.SetNestedMap(pod.Object, statusMap, "status")
return nil
}
}
type ResourceGetCallback func(resource *unstructured.Unstructured) error
type FakeClient struct {
resourceCallbackMap map[string]ResourceGetCallback
}
func (f *FakeClient) Get(_ context.Context, _ client.ObjectKey, obj runtime.Object) error {
kind := obj.GetObjectKind().GroupVersionKind().Kind
callbackFunc, found := f.resourceCallbackMap[kind]
if !found {
return fmt.Errorf("no callback func found for kind %s", kind)
}
u := obj.(*unstructured.Unstructured)
return callbackFunc(u)
}
func (f *FakeClient) List(ctx context.Context, list runtime.Object, opts ...client.ListOption) error {
return nil
}
func joinStatuses(statuses []status.Status) string {
var stringStatuses []string
for _, s := range statuses {
stringStatuses = append(stringStatuses, s.String())
}
return strings.Join(stringStatuses, ",")
}

View File

@@ -179,10 +179,13 @@ func (s *TablePrinter) printTable(data StatusData, deleteUp bool) {
}
func (s *TablePrinter) printTableRow(rowData []RowData) {
for _, row := range rowData {
for i, row := range rowData {
setColor(s.out, row.color)
format := fmt.Sprintf("%%-%ds ", row.width)
format := fmt.Sprintf("%%-%ds", row.width)
printOrDie(s.out, format, trimString(row.content, row.width))
if i != len(rowData)-1 {
printOrDie(s.out, " ")
}
setColor(s.out, WHITE)
}
printOrDie(s.out, "\n")

View File

@@ -22,9 +22,11 @@ func init() {
_ = clientgoscheme.AddToScheme(scheme)
}
// getClient returns a client for talking to a Kubernetes cluster. The client
type createClientFunc func() (client.Reader, error)
// createClient returns a client for talking to a Kubernetes cluster. The client
// is from controller-runtime.
func getClient() (client.Client, error) {
func createClient() (client.Reader, error) {
config := ctrl.GetConfigOrDie()
mapper, err := apiutil.NewDiscoveryRESTMapper(config)
if err != nil {
@@ -33,6 +35,12 @@ func getClient() (client.Client, error) {
return client.New(config, client.Options{Scheme: scheme, Mapper: mapper})
}
func newClientFunc(c client.Reader) func() (client.Reader, error) {
return func() (client.Reader, error) {
return c, nil
}
}
// CaptureIdentifiersFilter implements the Filter interface in the kio package. It
// captures the identifiers for all resources passed through the pipeline.
type CaptureIdentifiersFilter struct {

View File

@@ -18,7 +18,9 @@ import (
// GetWaitRunner return a command WaitRunner.
func GetWaitRunner() *WaitRunner {
r := &WaitRunner{}
r := &WaitRunner{
createClientFunc: createClient,
}
c := &cobra.Command{
Use: "wait DIR...",
Short: commands.WaitShort,
@@ -48,6 +50,8 @@ type WaitRunner struct {
Interval time.Duration
Timeout time.Duration
Command *cobra.Command
createClientFunc createClientFunc
}
// runE implements the logic of the command and will call the Wait command in the wait
@@ -55,7 +59,7 @@ type WaitRunner struct {
// TablePrinter to display the information.
func (r *WaitRunner) runE(c *cobra.Command, args []string) error {
ctx := context.Background()
client, err := getClient()
client, err := r.createClientFunc()
if err != nil {
return errors.Wrap(err, "error creating client")
}

View File

@@ -0,0 +1,176 @@
package cmd
import (
"bytes"
"fmt"
"reflect"
"testing"
"github.com/acarl005/stripansi"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kstatus/status"
)
func TestWaitNoResources(t *testing.T) {
inBuffer := &bytes.Buffer{}
outBuffer := &bytes.Buffer{}
fakeClient := &FakeClient{}
r := GetWaitRunner()
r.createClientFunc = newClientFunc(fakeClient)
r.Command.SetArgs([]string{})
r.Command.SetIn(inBuffer)
r.Command.SetOut(outBuffer)
err := r.Command.Execute()
if !assert.NoError(t, err) {
return
}
cleanOutput := stripansi.Strip(outBuffer.String())
tableOutput := parseTableOutput(t, cleanOutput)
if want, got := 2, len(tableOutput.Frames); want != got {
t.Errorf("expected %d frames, but found %d", want, got)
}
aggStatuses := tableOutput.allAggStatuses()
expectedAggStatuses := []status.Status{
status.CurrentStatus,
status.CurrentStatus,
}
if !reflect.DeepEqual(aggStatuses, expectedAggStatuses) {
t.Errorf("expected agg statuses to be %s, but got %s", joinStatuses(expectedAggStatuses),
joinStatuses(aggStatuses))
}
resources := tableOutput.resources()
if want, got := 0, len(resources); want != got {
t.Errorf("expected %d resources, but found %d", want, got)
}
}
func TestWaitMultipleUpdates(t *testing.T) {
inBuffer := &bytes.Buffer{}
outBuffer := &bytes.Buffer{}
_, err := fmt.Fprint(inBuffer, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: bar
namespace: default
`)
if !assert.NoError(t, err) {
return
}
fakeClient := &FakeClient{
resourceCallbackMap: map[string]ResourceGetCallback{
"Deployment": createDeploymentStatusFunc(),
},
}
r := GetWaitRunner()
r.createClientFunc = newClientFunc(fakeClient)
r.Command.SetArgs([]string{})
r.Command.SetIn(inBuffer)
r.Command.SetOut(outBuffer)
err = r.Command.Execute()
if !assert.NoError(t, err) {
return
}
cleanOutput := stripansi.Strip(outBuffer.String())
tableOutput := parseTableOutput(t, cleanOutput)
aggStatuses := tableOutput.dedupedAggStatuses()
expectedStatuses := []status.Status{
status.UnknownStatus,
status.InProgressStatus,
status.CurrentStatus,
}
if !reflect.DeepEqual(aggStatuses, expectedStatuses) {
t.Errorf("expected deduped agg statuses to be %s, but got %s", joinStatuses(expectedStatuses),
joinStatuses(aggStatuses))
}
resources := tableOutput.resources()
if want, got := 1, len(resources); got != want {
t.Errorf("expected %d resource, but got %d", want, got)
}
resource := resources[0]
resourceStatuses := tableOutput.dedupedStatusesForResource(resource)
expectedResourceStatuses := []status.Status{
status.InProgressStatus,
status.CurrentStatus,
}
if !reflect.DeepEqual(expectedResourceStatuses, resourceStatuses) {
t.Errorf("expected resource %q to have statuses %s, but got %s", resource.name,
joinStatuses(expectedResourceStatuses), joinStatuses(resourceStatuses))
}
}
func TestWaitMultipleResources(t *testing.T) {
inBuffer := &bytes.Buffer{}
outBuffer := &bytes.Buffer{}
_, err := fmt.Fprint(inBuffer, `
apiVersion: v1
kind: List
items:
- apiVersion: v1
kind: Pod
metadata:
name: foo
namespace: default
- apiVersion: v1
kind: Service
metadata:
name: bar
namespace: default
`)
if !assert.NoError(t, err) {
return
}
fakeClient := &FakeClient{
resourceCallbackMap: map[string]ResourceGetCallback{
"Pod": createPodStatusFunc(),
"Service": createServiceStatusFunc(),
},
}
r := GetWaitRunner()
r.createClientFunc = newClientFunc(fakeClient)
r.Command.SetArgs([]string{})
r.Command.SetIn(inBuffer)
r.Command.SetOut(outBuffer)
err = r.Command.Execute()
if !assert.NoError(t, err) {
return
}
cleanOutput := stripansi.Strip(outBuffer.String())
tableOutput := parseTableOutput(t, cleanOutput)
aggStatuses := tableOutput.dedupedAggStatuses()
if want, got := status.CurrentStatus, aggStatuses[len(aggStatuses)-1]; want != got {
t.Errorf("expected final agg statuses to be %s, but got %s", want, got)
}
resources := tableOutput.resources()
if want, got := 2, len(resources); got != want {
t.Errorf("expected %d resource, but got %d", want, got)
}
for _, resource := range resources {
resourceStatuses := tableOutput.dedupedStatusesForResource(resource)
if want, got := status.CurrentStatus, resourceStatuses[len(resourceStatuses)-1]; want != got {
t.Errorf("expected resource %q to have final status %s, but got %s", resource.name, want, got)
}
}
}

View File

@@ -1,10 +1,8 @@
// Code generated by "mdtogo"; DO NOT EDIT.
package commands
var EventsShort=`[Alpha] Poll the cluster until all provided resources have become Current and list the status change events.`
var EventsLong=`
var EventsShort = `[Alpha] Poll the cluster until all provided resources have become Current and list the status change events.`
var EventsLong = `
[Alpha] Poll the cluster for the state of all the provided resources until either they have all become
Current or the timeout is reached. The output will be status change events.
@@ -14,15 +12,15 @@ on StdIn.
DIR:
Path to local directory. If not provided, input is expected on StdIn.
`
var EventsExamples=`
var EventsExamples = `
# Read resources from the filesystem and wait up to 1 minute for all of them to become Current
resource status events my-dir/
# Fetch all resources in the cluster and wait up to 5 minutes for all of them to become Current
kubectl get all --all-namespaces -oyaml | resource status events --timeout=5m`
var FetchShort=`[Alpha] Fetch the state of the provided resources from the cluster and display status in a table.`
var FetchLong=`
var FetchShort = `[Alpha] Fetch the state of the provided resources from the cluster and display status in a table.`
var FetchLong = `
[Alpha] Fetches the state of all provided resources from the cluster and displays the status in
a table.
@@ -31,15 +29,15 @@ The list of resources are provided as manifests either on the filesystem or on S
DIR:
Path to local directory.
`
var FetchExamples=`
var FetchExamples = `
# Read resources from the filesystem and wait up to 1 minute for all of them to become Current
resource status fetch my-dir/
# Fetch all resources in the cluster and wait up to 5 minutes for all of them to become Current
kubectl get all --all-namespaces -oyaml | resource status fetch`
var WaitShort=`[Alpha] Poll the cluster until all provided resources have become Current and display progress in a table. `
var WaitLong=`
var WaitShort = `[Alpha] Poll the cluster until all provided resources have become Current and display progress in a table. `
var WaitLong = `
[Alpha] Poll the cluster for the state of all the provided resources until either they have all become
Current or the timeout is reached. The output will be presented as a table.
@@ -49,7 +47,7 @@ on StdIn.
DIR:
Path to local directory. If not provided, input is expected on StdIn.
`
var WaitExamples=`
var WaitExamples = `
# Read resources from the filesystem and wait up to 1 minute for all of them to become Current
resource status wait my-dir/

View File

@@ -4,6 +4,7 @@
package status
import (
"fmt"
"time"
"github.com/pkg/errors"
@@ -26,6 +27,11 @@ const (
UnknownStatus Status = "Unknown"
)
var (
Statuses = []Status{InProgressStatus, FailedStatus, CurrentStatus, TerminatingStatus, UnknownStatus}
ConditionTypes = []ConditionType{ConditionFailed, ConditionInProgress}
)
// ConditionType defines the set of condition types allowed inside a Condition struct.
type ConditionType string
@@ -42,6 +48,18 @@ func (s Status) String() string {
return string(s)
}
// StatusFromString turns a string into a Status. Will panic if the provided string is
// not a valid status.
func FromStringOrDie(text string) Status {
s := Status(text)
for _, r := range Statuses {
if s == r {
return s
}
}
panic(fmt.Errorf("string has invalid status: %s", s))
}
// Result contains the results of a call to compute the status of
// a resource.
type Result struct {