diff --git a/kstatus/.golangci.yml b/kstatus/.golangci.yml new file mode 100644 index 000000000..0f4759a8f --- /dev/null +++ b/kstatus/.golangci.yml @@ -0,0 +1,54 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 + +run: + deadline: 5m + +linters: + # please, do not use `enable-all`: it's deprecated and will be removed soon. + # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint + disable-all: true + enable: + - bodyclose + - deadcode + - depguard + - dogsled + - dupl + - errcheck + # - funlen + - gochecknoinits + - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - golint + - gosec + - gosimple + - govet + - ineffassign + - interfacer + - lll + - misspell + - nakedret + - scopelint + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - varcheck + - whitespace + + +linters-settings: + dupl: + threshold: 400 + lll: + line-length: 170 + gocyclo: + min-complexity: 30 + golint: + min-confidence: 0.85 diff --git a/kstatus/LICENSE_TEMPLATE b/kstatus/LICENSE_TEMPLATE new file mode 100644 index 000000000..0c2b3b655 --- /dev/null +++ b/kstatus/LICENSE_TEMPLATE @@ -0,0 +1,2 @@ +Copyright {{.Year}} {{.Holder}} +SPDX-License-Identifier: Apache-2.0 diff --git a/kstatus/Makefile b/kstatus/Makefile new file mode 100644 index 000000000..8c2f8e0b5 --- /dev/null +++ b/kstatus/Makefile @@ -0,0 +1,34 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 + +.PHONY: generate license fix vet fmt test lint tidy + +GOPATH := $(shell go env GOPATH) + +all: generate license fix vet fmt test lint tidy + +fix: + go fix ./... + +fmt: + go fmt ./... + +generate: + go generate ./... + +license: + (which $(GOPATH)/bin/addlicense || go get github.com/google/addlicense) + $(GOPATH)/bin/addlicense -y 2019 -c "The Kubernetes Authors." -f LICENSE_TEMPLATE . + +tidy: + go mod tidy + +lint: + (which $(GOPATH)/bin/golangci-lint || go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.19.1) + $(GOPATH)/bin/golangci-lint run ./... + +test: + go test -cover ./... + +vet: + go vet ./... diff --git a/kstatus/doc.go b/kstatus/doc.go new file mode 100644 index 000000000..f9d2999cb --- /dev/null +++ b/kstatus/doc.go @@ -0,0 +1,19 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +// Package kstatus contains libraries for computing status of kubernetes +// resources. +// +// Status +// Get status and/or conditions for resources based on resources already +// read from a cluster, i.e. it will not fetch resources from +// a cluster. +// +// Wait +// Get status and/or conditions for resources by fetching them +// from a cluster. This supports specifying a set of resources as +// an Inventory or as a list of manifests/unstructureds. This also +// supports polling the state of resources until they all reach a +// specific status. A common use case for this can be to wait for +// a set of resources to all finish reconciling after an apply. +package kstatus diff --git a/kstatus/go.mod b/kstatus/go.mod new file mode 100644 index 000000000..74a6cb5b5 --- /dev/null +++ b/kstatus/go.mod @@ -0,0 +1,16 @@ +module sigs.k8s.io/kustomize/kstatus + +go 1.12 + +require ( + github.com/ghodss/yaml v1.0.0 + github.com/kr/pretty v0.1.0 // indirect + github.com/pkg/errors v0.8.1 + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.4.0 + golang.org/x/net v0.0.0-20190909003024-a7b16738d86b // indirect + golang.org/x/text v0.3.2 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + sigs.k8s.io/kustomize/pseudo/k8s v0.1.0 + sigs.k8s.io/yaml v1.1.0 +) diff --git a/kstatus/go.sum b/kstatus/go.sum new file mode 100644 index 000000000..94a51155c --- /dev/null +++ b/kstatus/go.sum @@ -0,0 +1,137 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.9.2/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680 h1:ZktWZesgun21uEDrwW7iEV1zPCGQldM2atlJZ3TdvVM= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.6.0/go.mod h1:GICNByuaEBibcjmjvI7QvYJSZEbGkcYwAR7EZK2WMqM= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +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-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/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/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff h1:VARhShG49tiji6mdRNp7JTNDtJ0FhuprF93GBQ37xGU= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/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-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190909003024-a7b16738d86b h1:XfVGCX+0T4WOStkaOsJRllbsiImhB2jgVBGc9L0lPGc= +golang.org/x/net v0.0.0-20190909003024-a7b16738d86b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/utils v0.0.0-20191030222137-2b95a09bc58d/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +sigs.k8s.io/kustomize/pseudo/k8s v0.1.0 h1:otg4dLFc03c3gzl+2CV8GPGcd1kk8wjXwD+UhhcCn5I= +sigs.k8s.io/kustomize/pseudo/k8s v0.1.0/go.mod h1:bl/gVJgYYhJZCZdYU2BfnaKYAlqFkgbJEkpl302jEss= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/kstatus/status/core.go b/kstatus/status/core.go new file mode 100644 index 000000000..fdc951d79 --- /dev/null +++ b/kstatus/status/core.go @@ -0,0 +1,490 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package status + +import ( + "fmt" + + corev1 "sigs.k8s.io/kustomize/pseudo/k8s/api/core/v1" + "sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// GetConditionsFn defines the signature for functions to compute the +// status of a built-in resource. +type GetConditionsFn func(*unstructured.Unstructured) (*Result, error) + +// legacyTypes defines the mapping from GroupKind to a function that can +// compute the status for the given resource. +var legacyTypes = map[string]GetConditionsFn{ + "Service": serviceConditions, + "Pod": podConditions, + "Secret": alwaysReady, + "PersistentVolumeClaim": pvcConditions, + "apps/StatefulSet": stsConditions, + "apps/DaemonSet": daemonsetConditions, + "apps/Deployment": deploymentConditions, + "apps/ReplicaSet": replicasetConditions, + "policy/PodDisruptionBudget": pdbConditions, + "batch/CronJob": alwaysReady, + "ConfigMap": alwaysReady, + "batch/Job": jobConditions, +} + +const ( + tooFewReady = "LessReady" + tooFewAvailable = "LessAvailable" + tooFewUpdated = "LessUpdated" + tooFewReplicas = "LessReplicas" +) + +// GetLegacyConditionsFn returns a function that can compute the status for the +// given resource, or nil if the resource type is not known. +func GetLegacyConditionsFn(u *unstructured.Unstructured) GetConditionsFn { + gvk := u.GroupVersionKind() + g := gvk.Group + k := gvk.Kind + key := g + "/" + k + if g == "" { + key = k + } + return legacyTypes[key] +} + +// alwaysReady Used for resources that are always ready +func alwaysReady(u *unstructured.Unstructured) (*Result, error) { + return &Result{ + Status: CurrentStatus, + Message: "Resource is always ready", + Conditions: []Condition{}, + }, nil +} + +// stsConditions return standardized Conditions for Statefulset +// +// StatefulSet does define the .status.conditions property, but the controller never +// actually sets any Conditions. Thus, status must be computed only based on the other +// properties under .status. We don't have any way to find out if a reconcile for a +// StatefulSet has failed. +func stsConditions(u *unstructured.Unstructured) (*Result, error) { + obj := u.UnstructuredContent() + + // updateStrategy==ondelete is a user managed statefulset. + updateStrategy := GetStringField(obj, ".spec.updateStrategy.type", "") + if updateStrategy == "ondelete" { + return &Result{ + Status: CurrentStatus, + Message: "StatefulSet is using the ondelete update strategy", + Conditions: []Condition{}, + }, nil + } + + // Replicas + specReplicas := GetIntField(obj, ".spec.replicas", 1) + readyReplicas := GetIntField(obj, ".status.readyReplicas", 0) + currentReplicas := GetIntField(obj, ".status.currentReplicas", 0) + updatedReplicas := GetIntField(obj, ".status.updatedReplicas", 0) + statusReplicas := GetIntField(obj, ".status.replicas", 0) + partition := GetIntField(obj, ".spec.updateStrategy.rollingUpdate.partition", -1) + + if specReplicas > statusReplicas { + message := fmt.Sprintf("Replicas: %d/%d", statusReplicas, specReplicas) + return newInProgressStatus(tooFewReplicas, message), nil + } + + if specReplicas > readyReplicas { + message := fmt.Sprintf("Ready: %d/%d", readyReplicas, specReplicas) + return newInProgressStatus(tooFewReady, message), nil + } + + // https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#partitions + if partition != -1 { + if updatedReplicas < (specReplicas - partition) { + message := fmt.Sprintf("updated: %d/%d", updatedReplicas, specReplicas-partition) + return newInProgressStatus("PartitionRollout", message), nil + } + // Partition case All ok + return &Result{ + Status: CurrentStatus, + Message: fmt.Sprintf("Partition rollout complete. updated: %d", updatedReplicas), + Conditions: []Condition{}, + }, nil + } + + if specReplicas > currentReplicas { + message := fmt.Sprintf("current: %d/%d", currentReplicas, specReplicas) + return newInProgressStatus("LessCurrent", message), nil + } + + // Revision + currentRevision := GetStringField(obj, ".status.currentRevision", "") + updatedRevision := GetStringField(obj, ".status.updateRevision", "") + if currentRevision != updatedRevision { + message := "Waiting for updated revision to match current" + return newInProgressStatus("RevisionMismatch", message), nil + } + + // All ok + return &Result{ + Status: CurrentStatus, + Message: fmt.Sprintf("All replicas scheduled as expected. Replicas: %d", statusReplicas), + Conditions: []Condition{}, + }, nil +} + +// deploymentConditions return standardized Conditions for Deployment. +// +// For Deployments, we look at .status.conditions as well as the other properties +// under .status. Status will be Failed if the progress deadline has been exceeded. +func deploymentConditions(u *unstructured.Unstructured) (*Result, error) { + obj := u.UnstructuredContent() + + progressing := false + available := false + + objc, err := GetObjectWithConditions(obj) + if err != nil { + return nil, err + } + + for _, c := range objc.Status.Conditions { + switch c.Type { + case "Progressing": //appsv1.DeploymentProgressing: + // https://github.com/kubernetes/kubernetes/blob/a3ccea9d8743f2ff82e41b6c2af6dc2c41dc7b10/pkg/controller/deployment/progress.go#L52 + if c.Reason == "ProgressDeadlineExceeded" { + return &Result{ + Status: FailedStatus, + Message: "Progress deadline exceeded", + Conditions: []Condition{{ConditionFailed, corev1.ConditionTrue, c.Reason, c.Message}}, + }, nil + } + if c.Status == corev1.ConditionTrue && c.Reason == "NewReplicaSetAvailable" { + progressing = true + } + case "Available": //appsv1.DeploymentAvailable: + if c.Status == corev1.ConditionTrue { + available = true + } + } + } + + // replicas + specReplicas := GetIntField(obj, ".spec.replicas", 1) + statusReplicas := GetIntField(obj, ".status.replicas", 0) + updatedReplicas := GetIntField(obj, ".status.updatedReplicas", 0) + readyReplicas := GetIntField(obj, ".status.readyReplicas", 0) + availableReplicas := GetIntField(obj, ".status.availableReplicas", 0) + + // TODO spec.replicas zero case ?? + + if specReplicas > statusReplicas { + message := fmt.Sprintf("replicas: %d/%d", statusReplicas, specReplicas) + return newInProgressStatus(tooFewReplicas, message), nil + } + + if specReplicas > updatedReplicas { + message := fmt.Sprintf("Updated: %d/%d", updatedReplicas, specReplicas) + return newInProgressStatus(tooFewUpdated, message), nil + } + + if statusReplicas > updatedReplicas { + message := fmt.Sprintf("Pending termination: %d", statusReplicas-updatedReplicas) + return newInProgressStatus("ExtraPods", message), nil + } + + if updatedReplicas > availableReplicas { + message := fmt.Sprintf("Available: %d/%d", availableReplicas, updatedReplicas) + return newInProgressStatus(tooFewAvailable, message), nil + } + + if specReplicas > readyReplicas { + message := fmt.Sprintf("Ready: %d/%d", readyReplicas, specReplicas) + return newInProgressStatus(tooFewReady, message), nil + } + + // check conditions + if !progressing { + message := "ReplicaSet not Available" + return newInProgressStatus("ReplicaSetNotAvailable", message), nil + } + if !available { + message := "Deployment not Available" + return newInProgressStatus("DeploymentNotAvailable", message), nil + } + // All ok + return &Result{ + Status: CurrentStatus, + Message: fmt.Sprintf("Deployment is available. Replicas: %d", statusReplicas), + Conditions: []Condition{}, + }, nil +} + +// replicasetConditions return standardized Conditions for Replicaset +func replicasetConditions(u *unstructured.Unstructured) (*Result, error) { + obj := u.UnstructuredContent() + + // Conditions + objc, err := GetObjectWithConditions(obj) + if err != nil { + return nil, err + } + + for _, c := range objc.Status.Conditions { + // https://github.com/kubernetes/kubernetes/blob/a3ccea9d8743f2ff82e41b6c2af6dc2c41dc7b10/pkg/controller/replicaset/replica_set_utils.go + if c.Type == "ReplicaFailure" && c.Status == corev1.ConditionTrue { + message := "Replica Failure condition. Check Pods" + return newInProgressStatus("ReplicaFailure", message), nil + } + } + + // Replicas + specReplicas := GetIntField(obj, ".spec.replicas", 1) + statusReplicas := GetIntField(obj, ".status.replicas", 0) + readyReplicas := GetIntField(obj, ".status.readyReplicas", 0) + availableReplicas := GetIntField(obj, ".status.availableReplicas", 0) + labelledReplicas := GetIntField(obj, ".status.labelledReplicas", 0) + + if specReplicas == 0 && labelledReplicas == 0 && availableReplicas == 0 && readyReplicas == 0 { + message := ".spec.replica is 0" + return newInProgressStatus("ZeroReplicas", message), nil + } + + if specReplicas > labelledReplicas { + message := fmt.Sprintf("Labelled: %d/%d", labelledReplicas, specReplicas) + return newInProgressStatus("LessLabelled", message), nil + } + + if specReplicas > availableReplicas { + message := fmt.Sprintf("Available: %d/%d", availableReplicas, specReplicas) + return newInProgressStatus(tooFewAvailable, message), nil + } + + if specReplicas > readyReplicas { + message := fmt.Sprintf("Ready: %d/%d", readyReplicas, specReplicas) + return newInProgressStatus(tooFewReady, message), nil + } + + if specReplicas < statusReplicas { + message := fmt.Sprintf("replicas: %d/%d", statusReplicas, specReplicas) + return newInProgressStatus("ExtraPods", message), nil + } + // All ok + return &Result{ + Status: CurrentStatus, + Message: fmt.Sprintf("ReplicaSet is available. Replicas: %d", statusReplicas), + Conditions: []Condition{}, + }, nil +} + +// daemonsetConditions return standardized Conditions for DaemonSet +func daemonsetConditions(u *unstructured.Unstructured) (*Result, error) { + obj := u.UnstructuredContent() + + // replicas + desiredNumberScheduled := GetIntField(obj, ".status.desiredNumberScheduled", -1) + currentNumberScheduled := GetIntField(obj, ".status.currentNumberScheduled", 0) + updatedNumberScheduled := GetIntField(obj, ".status.updatedNumberScheduled", 0) + numberAvailable := GetIntField(obj, ".status.numberAvailable", 0) + numberReady := GetIntField(obj, ".status.numberReady", 0) + + if desiredNumberScheduled == -1 { + message := "Missing .status.desiredNumberScheduled" + return newInProgressStatus("NoDesiredNumber", message), nil + } + + if desiredNumberScheduled > currentNumberScheduled { + message := fmt.Sprintf("Current: %d/%d", currentNumberScheduled, desiredNumberScheduled) + return newInProgressStatus("LessCurrent", message), nil + } + + if desiredNumberScheduled > updatedNumberScheduled { + message := fmt.Sprintf("Updated: %d/%d", updatedNumberScheduled, desiredNumberScheduled) + return newInProgressStatus(tooFewUpdated, message), nil + } + + if desiredNumberScheduled > numberAvailable { + message := fmt.Sprintf("Available: %d/%d", numberAvailable, desiredNumberScheduled) + return newInProgressStatus(tooFewAvailable, message), nil + } + + if desiredNumberScheduled > numberReady { + message := fmt.Sprintf("Ready: %d/%d", numberReady, desiredNumberScheduled) + return newInProgressStatus(tooFewReady, message), nil + } + + // All ok + return &Result{ + Status: CurrentStatus, + Message: fmt.Sprintf("All replicas scheduled as expected. Replicas: %d", desiredNumberScheduled), + Conditions: []Condition{}, + }, nil +} + +// pvcConditions return standardized Conditions for PVC +func pvcConditions(u *unstructured.Unstructured) (*Result, error) { + obj := u.UnstructuredContent() + + phase := GetStringField(obj, ".status.phase", "unknown") + if phase != "Bound" { // corev1.ClaimBound + message := fmt.Sprintf("PVC is not Bound. phase: %s", phase) + return newInProgressStatus("NotBound", message), nil + } + // All ok + return &Result{ + Status: CurrentStatus, + Message: "PVC is Bound", + Conditions: []Condition{}, + }, nil +} + +// podConditions return standardized Conditions for Pod +func podConditions(u *unstructured.Unstructured) (*Result, error) { + obj := u.UnstructuredContent() + objc, err := GetObjectWithConditions(obj) + if err != nil { + return nil, err + } + phase := GetStringField(obj, ".status.phase", "unknown") + + if phase == "Succeeded" { + return &Result{ + Status: CurrentStatus, + Message: "Pod has completed successfully", + Conditions: []Condition{}, + }, nil + } + + for _, c := range objc.Status.Conditions { + if c.Type == "Ready" { + if c.Status == corev1.ConditionTrue { + return &Result{ + Status: CurrentStatus, + Message: "Pod has reached the ready state", + Conditions: []Condition{}, + }, nil + } + if c.Status == corev1.ConditionFalse && c.Reason == "PodCompleted" && phase != "Succeeded" { + message := "Pod has completed, but not successfully." + return &Result{ + Status: FailedStatus, + Message: message, + Conditions: []Condition{{ + Type: ConditionFailed, + Status: corev1.ConditionTrue, + Reason: "PodFailed", + Message: fmt.Sprintf("Pod has completed, but not succeesfully."), + }}, + }, nil + } + } + } + + message := "Pod has not become ready" + return newInProgressStatus("PodNotReady", message), nil +} + +// pdbConditions return standardized Conditions for Deployment +func pdbConditions(u *unstructured.Unstructured) (*Result, error) { + obj := u.UnstructuredContent() + + // replicas + currentHealthy := GetIntField(obj, ".status.currentHealthy", 0) + desiredHealthy := GetIntField(obj, ".status.desiredHealthy", 0) + if desiredHealthy == 0 { + message := "Missing or zero .status.desiredHealthy" + return newInProgressStatus("ZeroDesiredHealthy", message), nil + } + if desiredHealthy > currentHealthy { + message := fmt.Sprintf("Budget not met. healthy replicas: %d/%d", currentHealthy, desiredHealthy) + return newInProgressStatus("BudgetNotMet", message), nil + } + + // All ok + return &Result{ + Status: CurrentStatus, + Message: fmt.Sprintf("Budget is met. Replicas: %d/%d", currentHealthy, desiredHealthy), + Conditions: []Condition{}, + }, nil +} + +// jobConditions return standardized Conditions for Job +// +// A job will have the InProgress status until it starts running. Then it will have the Current +// status while the job is running and after it has been completed successfully. It +// will have the Failed status if it the job has failed. +func jobConditions(u *unstructured.Unstructured) (*Result, error) { + obj := u.UnstructuredContent() + + parallelism := GetIntField(obj, ".spec.parallelism", 1) + completions := GetIntField(obj, ".spec.completions", parallelism) + succeeded := GetIntField(obj, ".status.succeeded", 0) + active := GetIntField(obj, ".status.active", 0) + failed := GetIntField(obj, ".status.failed", 0) + starttime := GetStringField(obj, ".status.startTime", "") + + // Conditions + // https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/job/utils.go#L24 + objc, err := GetObjectWithConditions(obj) + if err != nil { + return nil, err + } + for _, c := range objc.Status.Conditions { + switch c.Type { + case "Complete": + if c.Status == corev1.ConditionTrue { + message := fmt.Sprintf("Job Completed. succeeded: %d/%d", succeeded, completions) + return &Result{ + Status: CurrentStatus, + Message: message, + Conditions: []Condition{}, + }, nil + } + case "Failed": + if c.Status == corev1.ConditionTrue { + message := fmt.Sprintf("Job Failed. failed: %d/%d", failed, completions) + return &Result{ + Status: FailedStatus, + Message: message, + Conditions: []Condition{{ + ConditionFailed, + corev1.ConditionTrue, + "JobFailed", + fmt.Sprintf("Job Failed. failed: %d/%d", failed, completions), + }}, + }, nil + } + } + } + + // replicas + if starttime == "" { + message := "Job not started" + return newInProgressStatus("JobNotStarted", message), nil + } + return &Result{ + Status: CurrentStatus, + Message: fmt.Sprintf("Job in progress. success:%d, active: %d, failed: %d", succeeded, active, failed), + Conditions: []Condition{}, + }, nil +} + +// serviceConditions return standardized Conditions for Service +func serviceConditions(u *unstructured.Unstructured) (*Result, error) { + obj := u.UnstructuredContent() + + specType := GetStringField(obj, ".spec.type", "ClusterIP") + specClusterIP := GetStringField(obj, ".spec.clusterIP", "") + + if specType == "LoadBalancer" { + if specClusterIP == "" { + message := "ClusterIP not set. Service type: LoadBalancer" + return newInProgressStatus("NoIPAssigned", message), nil + } + } + + return &Result{ + Status: CurrentStatus, + Message: "Service is ready", + Conditions: []Condition{}, + }, nil +} diff --git a/kstatus/status/doc.go b/kstatus/status/doc.go new file mode 100644 index 000000000..1cec69e49 --- /dev/null +++ b/kstatus/status/doc.go @@ -0,0 +1,38 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +// Package kstatus contains functionality for computing the status +// of Kubernetes resources. +// +// The statuses defined in this package is: +// * InProgress +// * Current +// * Failed +// * Terminating +// * Unknown +// +// Computing the status of a resources can be done by calling the +// Compute function in the status package. +// import ( +// "sigs.k8s.io/kustomize/kstatus/status +// ) +// res, err := status.Compute(resource) +// +// +// The package also defines a set of new conditions: +// * InProgress +// * Failed +// These conditions have been chosen to follow the +// "abnormal-true" pattern where conditions should be set to true +// for error/abnormal conditions and the absence of a condition means +// things are normal. +// +// The Augment function augments any unstructured resource with +// the standard conditions described above. The values of +// these conditions are decided based on other status information +// available in the resources. +// import ( +// "sigs.k8s.io/kustomize/kstatus/status +// ) +// err := status.Augment(resource) +package status diff --git a/kstatus/status/example_test.go b/kstatus/status/example_test.go new file mode 100644 index 000000000..5973dcc17 --- /dev/null +++ b/kstatus/status/example_test.go @@ -0,0 +1,110 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package status_test + +import ( + "fmt" + "log" + + . "sigs.k8s.io/kustomize/kstatus/status" + "sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/yaml" +) + +func ExampleCompute() { + deploymentManifest := ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test + generation: 1 + namespace: qual +status: + observedGeneration: 1 + updatedReplicas: 1 + readyReplicas: 1 + availableReplicas: 1 + replicas: 1 + conditions: + - type: Progressing + status: "True" + reason: NewReplicaSetAvailable + - type: Available + status: "True" +` + deployment := yamlManifestToUnstructured(deploymentManifest) + + res, err := Compute(deployment) + if err != nil { + log.Fatal(err) + } + fmt.Println(res.Status) + // Output: + // Current +} + +func ExampleAugment() { + deploymentManifest := ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test + generation: 1 + namespace: qual +status: + observedGeneration: 1 + updatedReplicas: 1 + readyReplicas: 1 + availableReplicas: 1 + replicas: 1 + conditions: + - type: Progressing + status: "True" + reason: NewReplicaSetAvailable + - type: Available + status: "True" +` + deployment := yamlManifestToUnstructured(deploymentManifest) + + err := Augment(deployment) + if err != nil { + log.Fatal(err) + } + b, err := yaml.Marshal(deployment.Object) + if err != nil { + log.Fatal(err) + } + fmt.Println(string(b)) + // Output: + // apiVersion: apps/v1 + // kind: Deployment + // metadata: + // generation: 1 + // name: test + // namespace: qual + // status: + // availableReplicas: 1 + // conditions: + // - reason: NewReplicaSetAvailable + // status: "True" + // type: Progressing + // - status: "True" + // type: Available + // observedGeneration: 1 + // readyReplicas: 1 + // replicas: 1 + // updatedReplicas: 1 +} + +func yamlManifestToUnstructured(manifest string) *unstructured.Unstructured { + jsonManifest, err := yaml.YAMLToJSON([]byte(manifest)) + if err != nil { + log.Fatal(err) + } + resource, _, err := unstructured.UnstructuredJSONScheme.Decode(jsonManifest, nil, nil) + if err != nil { + log.Fatal(err) + } + return resource.(*unstructured.Unstructured) +} diff --git a/kstatus/status/generic.go b/kstatus/status/generic.go new file mode 100644 index 000000000..52cee50f8 --- /dev/null +++ b/kstatus/status/generic.go @@ -0,0 +1,92 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package status + +import ( + "fmt" + + "github.com/pkg/errors" + corev1 "sigs.k8s.io/kustomize/pseudo/k8s/api/core/v1" + "sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// checkGenericProperties looks at the properties that are available on +// all or most of the Kubernetes resources. If a decision can be made based +// on this information, there is no need to look at the resource-specidic +// rules. +// This also checks for the presence of the conditions defined in this package. +// If any of these are set on the resource, a decision is made solely based +// on this and none of the resource specific rules will be used. The goal here +// is that if controllers, built-in or custom, use these conditions, we can easily +// find status of resources. +func checkGenericProperties(u *unstructured.Unstructured) (*Result, error) { + obj := u.UnstructuredContent() + + // Check if the resource is scheduled for deletion + deletionTimestamp, found, err := unstructured.NestedString(obj, "metadata", "deletionTimestamp") + if err != nil { + return nil, errors.Wrap(err, "looking up metadata.deletionTimestamp from resource") + } + if found && deletionTimestamp != "" { + return &Result{ + Status: TerminatingStatus, + Message: "Resource scheduled for deletion", + Conditions: []Condition{}, + }, nil + } + + // ensure that the meta generation is observed + generation, found, err := unstructured.NestedInt64(u.Object, "metadata", "generation") + if err != nil { + return nil, errors.Wrap(err, "looking up metadata.generation from resource") + } + if !found { + return nil, errors.New("unable to find metadata.generation from resource") + } + observedGeneration, found, err := unstructured.NestedInt64(u.Object, "status", "observedGeneration") + if err != nil { + return nil, errors.Wrap(err, "looking up status.observedGeneration from resource") + } + if found { + // Resource does not have this field, so we can't do this check. + // TODO(mortent): Verify behavior of not set vs does not exist. + if observedGeneration != generation { + message := fmt.Sprintf("%s generation is %d, but latest observed generation is %d", u.GetKind(), generation, observedGeneration) + return &Result{ + Status: InProgressStatus, + Message: message, + Conditions: []Condition{newInProgressCondition("LatestGenerationNotObserved", message)}, + }, nil + } + } + + // Check if the resource has any of the standard conditions. If so, we just use them + // and no need to look at anything else. + objWithConditions, err := GetObjectWithConditions(obj) + if err != nil { + return nil, err + } + + for _, cond := range objWithConditions.Status.Conditions { + if cond.Type == string(ConditionInProgress) && cond.Status == corev1.ConditionTrue { + return newInProgressStatus(cond.Reason, cond.Message), nil + } + if cond.Type == string(ConditionFailed) && cond.Status == corev1.ConditionTrue { + return &Result{ + Status: FailedStatus, + Message: cond.Message, + Conditions: []Condition{ + { + Type: ConditionFailed, + Status: corev1.ConditionTrue, + Reason: cond.Reason, + Message: cond.Message, + }, + }, + }, nil + } + } + + return nil, nil +} diff --git a/kstatus/status/status.go b/kstatus/status/status.go new file mode 100644 index 000000000..b2401764f --- /dev/null +++ b/kstatus/status/status.go @@ -0,0 +1,161 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package status + +import ( + "time" + + "github.com/pkg/errors" + corev1 "sigs.k8s.io/kustomize/pseudo/k8s/api/core/v1" + "sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/apis/meta/v1/unstructured" +) + +const ( + // The set of standard conditions defined in this package. These follow the "abnormality-true" + // convention where conditions should have a true value for abnormal/error situations and the absence + // of a condition should be interpreted as a false value, i.e. everything is normal. + ConditionFailed ConditionType = "Failed" + ConditionInProgress ConditionType = "InProgress" + + // The set of status conditions which can be assigned to resources. + InProgressStatus Status = "InProgress" + FailedStatus Status = "Failed" + CurrentStatus Status = "Current" + TerminatingStatus Status = "Terminating" +) + +// ConditionType defines the set of condition types allowed inside a Condition struct. +type ConditionType string + +// String returns the ConditionType as a string. +func (c ConditionType) String() string { + return string(c) +} + +// Status defines the set of statuses a resource can have. +type Status string + +// String returns the status as a string. +func (s Status) String() string { + return string(s) +} + +// Result contains the results of a call to compute the status of +// a resource. +type Result struct { + //Status + Status Status + // Message + Message string + // Conditions list of extracted conditions from Resource + Conditions []Condition +} + +// Condition defines the general format for conditions on Kubernetes resources. +// In practice, each kubernetes resource defines their own format for conditions, but +// most (maybe all) follows this structure. +type Condition struct { + // Type condition type + Type ConditionType + // Status String that describes the condition status + Status corev1.ConditionStatus + // Reason one work CamelCase reason + Reason string + // Message Human readable reason string + Message string +} + +// Compute finds the status of a given unstructured resource. It does not +// fetch the state of the resource from a cluster, so the provided unstructured +// must have the complete state, including status. +// +// The returned result contains the status of the resource, which will be +// one of +// * InProgress +// * Current +// * Failed +// * Terminating +// It also contains a message that provides more information on why +// the resource has the given status. Finally, the result also contains +// a list of standard resources that would belong on the given resource. +func Compute(u *unstructured.Unstructured) (*Result, error) { + res, err := checkGenericProperties(u) + if err != nil || res != nil { + return res, err + } + + fn := GetLegacyConditionsFn(u) + if fn != nil { + return fn(u) + } + + // The resource is not one of the built-in types with specific + // rules and we were unable to make a decision based on the + // generic rules. In this case we assume that the absence of any known + // conditions means the resource is current. + return &Result{ + Status: CurrentStatus, + Message: "Resource is current", + Conditions: []Condition{}, + }, err +} + +// Augment takes a resource and augments the resource with the +// standard status conditions. +func Augment(u *unstructured.Unstructured) error { + res, err := Compute(u) + if err != nil { + return err + } + + conditions, found, err := unstructured.NestedSlice(u.Object, "status", "conditions") + if err != nil { + return err + } + + if !found { + conditions = make([]interface{}, 0) + } + + currentTime := time.Now().UTC().Format(time.RFC3339) + + for _, resCondition := range res.Conditions { + present := false + for _, c := range conditions { + condition, ok := c.(map[string]interface{}) + if !ok { + return errors.New("condition does not have the expected structure") + } + conditionType, ok := condition["type"].(string) + if !ok { + return errors.New("condition type does not have the expected type") + } + if conditionType == string(resCondition.Type) { + conditionStatus, ok := condition["status"].(string) + if !ok { + return errors.New("condition status does not have the expected type") + } + if conditionStatus != string(resCondition.Status) { + condition["lastTransitionTime"] = currentTime + } + condition["status"] = string(resCondition.Status) + condition["lastUpdateTime"] = currentTime + condition["reason"] = resCondition.Reason + condition["message"] = resCondition.Message + present = true + } + } + if !present { + conditions = append(conditions, map[string]interface{}{ + "lastTransitionTime": currentTime, + "lastUpdateTime": currentTime, + "message": resCondition.Message, + "reason": resCondition.Reason, + "status": string(resCondition.Status), + "type": string(resCondition.Type), + }) + } + } + return unstructured.SetNestedSlice(u.Object, conditions, "status", "conditions") +} diff --git a/kstatus/status/status_augment_test.go b/kstatus/status/status_augment_test.go new file mode 100644 index 000000000..9f7145bb2 --- /dev/null +++ b/kstatus/status/status_augment_test.go @@ -0,0 +1,166 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package status + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + corev1 "sigs.k8s.io/kustomize/pseudo/k8s/api/core/v1" + "sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/apis/meta/v1/unstructured" +) + +var pod = ` +apiVersion: v1 +kind: Pod +metadata: + generation: 1 + name: test + namespace: qual +status: + phase: Running +` + +var custom = ` +apiVersion: v1beta1 +kind: SomeCustomKind +metadata: + generation: 1 + name: test + namespace: default +` + +var timestamp = time.Now().Add(-1 * time.Minute).UTC().Format(time.RFC3339) + +func addConditions(t *testing.T, u *unstructured.Unstructured, conditions []map[string]interface{}) { + conds := make([]interface{}, 0) + for _, c := range conditions { + conds = append(conds, c) + } + err := unstructured.SetNestedSlice(u.Object, conds, "status", "conditions") + if err != nil { + t.Fatal(err) + } +} + +func TestAugmentConditions(t *testing.T) { + testCases := map[string]struct { + manifest string + withConditions []map[string]interface{} + expectedConditions []Condition + }{ + "no existing conditions": { + manifest: pod, + withConditions: []map[string]interface{}{}, + expectedConditions: []Condition{ + { + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "PodNotReady", + }, + }, + }, + "has other existing conditions": { + manifest: pod, + withConditions: []map[string]interface{}{ + { + "lastTransitionTime": timestamp, + "lastUpdateTime": timestamp, + "type": "Ready", + "status": "False", + "reason": "Pod has not started", + }, + }, + expectedConditions: []Condition{ + { + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "PodNotReady", + }, + { + Type: "Ready", + Status: corev1.ConditionFalse, + Reason: "Pod has not started", + }, + }, + }, + "already has condition of standard type InProgress": { + manifest: pod, + withConditions: []map[string]interface{}{ + { + "lastTransitionTime": timestamp, + "lastUpdateTime": timestamp, + "type": ConditionInProgress.String(), + "status": "True", + "reason": "PodIsAbsolutelyNotReady", + }, + }, + expectedConditions: []Condition{ + { + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "PodIsAbsolutelyNotReady", + }, + }, + }, + "already has condition of standard type Failed": { + manifest: pod, + withConditions: []map[string]interface{}{ + { + "lastTransitionTime": timestamp, + "lastUpdateTime": timestamp, + "type": ConditionFailed.String(), + "status": "True", + "reason": "PodHasFailed", + }, + }, + expectedConditions: []Condition{ + { + Type: ConditionFailed, + Status: corev1.ConditionTrue, + Reason: "PodHasFailed", + }, + }, + }, + "custom resource with no conditions": { + manifest: custom, + withConditions: []map[string]interface{}{}, + expectedConditions: []Condition{}, + }, + } + + for tn, tc := range testCases { + tc := tc + t.Run(tn, func(t *testing.T) { + u := y2u(t, tc.manifest) + addConditions(t, u, tc.withConditions) + + err := Augment(u) + if err != nil { + t.Error(err) + } + + obj, err := GetObjectWithConditions(u.Object) + if err != nil { + t.Error(err) + } + + assert.Equal(t, len(tc.expectedConditions), len(obj.Status.Conditions)) + + for _, expectedCondition := range tc.expectedConditions { + found := false + for _, condition := range obj.Status.Conditions { + if expectedCondition.Type.String() != condition.Type { + continue + } + found = true + assert.Equal(t, expectedCondition.Type.String(), condition.Type) + assert.Equal(t, expectedCondition.Reason, condition.Reason) + } + assert.True(t, found) + } + }) + } +} diff --git a/kstatus/status/status_compute_test.go b/kstatus/status/status_compute_test.go new file mode 100644 index 000000000..0bc80d8c0 --- /dev/null +++ b/kstatus/status/status_compute_test.go @@ -0,0 +1,1273 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package status + +import ( + "testing" + + "github.com/ghodss/yaml" + "github.com/stretchr/testify/assert" + corev1 "sigs.k8s.io/kustomize/pseudo/k8s/api/core/v1" + "sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/apis/meta/v1/unstructured" +) + +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) +} + +type testSpec struct { + spec string + expectedStatus Status + expectedConditions []Condition + absentConditionTypes []ConditionType +} + +func runStatusTest(t *testing.T, tc testSpec) { + res, err := Compute(y2u(t, tc.spec)) + assert.NoError(t, err) + assert.Equal(t, tc.expectedStatus, res.Status) + + for _, expectedCondition := range tc.expectedConditions { + found := false + for _, condition := range res.Conditions { + if condition.Type != expectedCondition.Type { + continue + } + found = true + assert.Equal(t, expectedCondition.Status, condition.Status) + assert.Equal(t, expectedCondition.Reason, condition.Reason) + } + if !found { + t.Errorf("Expected condition of type %s, but didn't find it", expectedCondition.Type) + } + } + + for _, absentConditionType := range tc.absentConditionTypes { + for _, condition := range res.Conditions { + if condition.Type == absentConditionType { + t.Errorf("Expected condition %s to be absent, but found it", absentConditionType) + } + } + } +} + +var podNoStatus = ` +apiVersion: v1 +kind: Pod +metadata: + generation: 1 + name: test +` + +var podReady = ` +apiVersion: v1 +kind: Pod +metadata: + generation: 1 + name: test + namespace: qual +status: + conditions: + - type: Ready + status: "True" + phase: Running +` + +var podCompletedOK = ` +apiVersion: v1 +kind: Pod +metadata: + generation: 1 + name: test + namespace: qual +status: + phase: Succeeded + conditions: + - type: Ready + status: "False" + reason: PodCompleted + +` + +var podCompletedFail = ` +apiVersion: v1 +kind: Pod +metadata: + generation: 1 + name: test + namespace: qual +status: + phase: Failed + conditions: + - type: Ready + status: "False" + reason: PodCompleted +` + +// Test coverage using GetConditions +func TestPodStatus(t *testing.T) { + testCases := map[string]testSpec{ + "podNoStatus": { + spec: podNoStatus, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "PodNotReady", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "podReady": { + spec: podReady, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionInProgress, + ConditionFailed, + }, + }, + "podCompletedSuccessfully": { + spec: podCompletedOK, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionInProgress, + ConditionFailed, + }, + }, + "podCompletedFailed": { + spec: podCompletedFail, + expectedStatus: FailedStatus, + expectedConditions: []Condition{{ + Type: ConditionFailed, + Status: corev1.ConditionTrue, + Reason: "PodFailed", + }}, + absentConditionTypes: []ConditionType{ + ConditionInProgress, + }, + }, + } + + for tn, tc := range testCases { + tc := tc + t.Run(tn, func(t *testing.T) { + runStatusTest(t, tc) + }) + } +} + +var pvcNoStatus = ` +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + generation: 1 + name: test +` +var pvcBound = ` +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + generation: 1 + name: test + namespace: qual +status: + phase: Bound +` + +func TestPVCStatus(t *testing.T) { + testCases := map[string]testSpec{ + "pvcNoStatus": { + spec: pvcNoStatus, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "NotBound", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "pvcBound": { + spec: pvcBound, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + } + + for tn, tc := range testCases { + tc := tc + t.Run(tn, func(t *testing.T) { + runStatusTest(t, tc) + }) + } +} + +var stsNoStatus = ` +apiVersion: apps/v1 +kind: StatefulSet +metadata: + generation: 1 + name: test +` +var stsBadStatus = ` +apiVersion: apps/v1 +kind: StatefulSet +metadata: + generation: 1 + name: test + namespace: qual +status: + observedGeneration: 1 + currentReplicas: 1 +` + +var stsOK = ` +apiVersion: apps/v1 +kind: StatefulSet +metadata: + generation: 1 + name: test + namespace: qual +spec: + replicas: 4 +status: + observedGeneration: 1 + currentReplicas: 4 + readyReplicas: 4 + replicas: 4 +` + +var stsLessReady = ` +apiVersion: apps/v1 +kind: StatefulSet +metadata: + generation: 1 + name: test + namespace: qual +spec: + replicas: 4 +status: + observedGeneration: 1 + currentReplicas: 4 + readyReplicas: 2 + replicas: 4 +` +var stsLessCurrent = ` +apiVersion: apps/v1 +kind: StatefulSet +metadata: + generation: 1 + name: test + namespace: qual +spec: + replicas: 4 +status: + observedGeneration: 1 + currentReplicas: 2 + readyReplicas: 4 + replicas: 4 +` + +func TestStsStatus(t *testing.T) { + testCases := map[string]testSpec{ + "stsNoStatus": { + spec: stsNoStatus, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "LessReplicas", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "stsBadStatus": { + spec: stsBadStatus, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "LessReplicas", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "stsOK": { + spec: stsOK, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "stsLessReady": { + spec: stsLessReady, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "LessReady", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "stsLessCurrent": { + spec: stsLessCurrent, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "LessCurrent", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + } + + for tn, tc := range testCases { + tc := tc + t.Run(tn, func(t *testing.T) { + runStatusTest(t, tc) + }) + } +} + +var dsNoStatus = ` +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: test + generation: 1 +` +var dsBadStatus = ` +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: test + namespace: qual + generation: 1 +status: + observedGeneration: 1 + currentReplicas: 1 +` + +var dsOK = ` +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: test + namespace: qual + generation: 1 +status: + desiredNumberScheduled: 4 + currentNumberScheduled: 4 + updatedNumberScheduled: 4 + numberAvailable: 4 + numberReady: 4 + observedGeneration: 1 +` + +var dsLessReady = ` +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: test + namespace: qual + generation: 1 +status: + observedGeneration: 1 + desiredNumberScheduled: 4 + currentNumberScheduled: 4 + updatedNumberScheduled: 4 + numberAvailable: 4 + numberReady: 2 +` +var dsLessAvailable = ` +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: test + namespace: qual + generation: 1 +status: + observedGeneration: 1 + desiredNumberScheduled: 4 + currentNumberScheduled: 4 + updatedNumberScheduled: 4 + numberAvailable: 2 + numberReady: 4 +` + +func TestDaemonsetStatus(t *testing.T) { + testCases := map[string]testSpec{ + "dsNoStatus": { + spec: dsNoStatus, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "NoDesiredNumber", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "dsBadStatus": { + spec: dsBadStatus, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "NoDesiredNumber", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "dsOK": { + spec: dsOK, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "dsLessReady": { + spec: dsLessReady, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "LessReady", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "dsLessAvailable": { + spec: dsLessAvailable, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "LessAvailable", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + } + + for tn, tc := range testCases { + tc := tc + t.Run(tn, func(t *testing.T) { + runStatusTest(t, tc) + }) + } +} + +var depNoStatus = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test + generation: 1 +` + +var depOK = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test + generation: 1 + namespace: qual +status: + observedGeneration: 1 + updatedReplicas: 1 + readyReplicas: 1 + availableReplicas: 1 + replicas: 1 + conditions: + - type: Progressing + status: "True" + reason: NewReplicaSetAvailable + - type: Available + status: "True" +` + +var depNotProgressing = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test + generation: 1 + namespace: qual +status: + observedGeneration: 1 + updatedReplicas: 1 + readyReplicas: 1 + availableReplicas: 1 + replicas: 1 + observedGeneration: 1 + conditions: + - type: Progressing + status: "False" + reason: Some reason + - type: Available + status: "True" +` + +var depNotAvailable = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test + generation: 1 + namespace: qual +status: + observedGeneration: 1 + updatedReplicas: 1 + readyReplicas: 1 + availableReplicas: 1 + replicas: 1 + observedGeneration: 1 + conditions: + - type: Progressing + status: "True" + reason: NewReplicaSetAvailable + - type: Available + status: "False" +` + +func TestDeploymentStatus(t *testing.T) { + testCases := map[string]testSpec{ + "depNoStatus": { + spec: depNoStatus, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "LessReplicas", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "depOK": { + spec: depOK, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "depNotProgressing": { + spec: depNotProgressing, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "ReplicaSetNotAvailable", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "depNotAvailable": { + spec: depNotAvailable, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "DeploymentNotAvailable", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + } + + for tn, tc := range testCases { + tc := tc + t.Run(tn, func(t *testing.T) { + runStatusTest(t, tc) + }) + } +} + +var rsNoStatus = ` +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: test + generation: 1 +` + +var rsOK1 = ` +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: test + namespace: qual + generation: 1 +spec: + replicas: 2 +status: + observedGeneration: 1 + replicas: 2 + readyReplicas: 2 + availableReplicas: 2 + labelledReplicas: 2 + conditions: + - type: ReplicaFailure + status: "False" +` + +var rsOK2 = ` +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: test + namespace: qual + generation: 1 +spec: + replicas: 2 +status: + observedGeneration: 1 + labelledReplicas: 2 + replicas: 2 + readyReplicas: 2 + availableReplicas: 2 +` + +var rsLessReady = ` +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: test + namespace: qual + generation: 1 +spec: + replicas: 4 +status: + observedGeneration: 1 + replicas: 4 + readyReplicas: 2 + availableReplicas: 4 + labelledReplicas: 4 +` + +var rsLessAvailable = ` +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: test + namespace: qual + generation: 1 +spec: + replicas: 4 +status: + observedGeneration: 1 + replicas: 4 + readyReplicas: 4 + availableReplicas: 2 + labelledReplicas: 4 +` + +var rsReplicaFailure = ` +apiVersion: apps/v1 +kind: ReplicaSet +metadata: + name: test + namespace: qual + generation: 1 +spec: + replicas: 4 +status: + observedGeneration: 1 + replicas: 4 + readyReplicas: 4 + labelledReplicas: 4 + availableReplicas: 4 + conditions: + - type: ReplicaFailure + status: "True" +` + +func TestReplicasetStatus(t *testing.T) { + testCases := map[string]testSpec{ + "rsNoStatus": { + spec: rsNoStatus, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "LessLabelled", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "rsOK1": { + spec: rsOK1, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "rsOK2": { + spec: rsOK2, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "rsLessAvailable": { + spec: rsLessAvailable, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "LessAvailable", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "rsLessReady": { + spec: rsLessReady, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "LessReady", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "rsReplicaFailure": { + spec: rsReplicaFailure, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "ReplicaFailure", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + } + + for tn, tc := range testCases { + tc := tc + t.Run(tn, func(t *testing.T) { + runStatusTest(t, tc) + }) + } +} + +var pdbNoStatus = ` +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + generation: 1 + name: test +` + +var pdbOK1 = ` +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + generation: 1 + name: test + namespace: qual +status: + currentHealthy: 2 + desiredHealthy: 2 +` + +var pdbMoreHealthy = ` +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + generation: 1 + name: test + namespace: qual +status: + currentHealthy: 4 + desiredHealthy: 2 +` + +var pdbLessHealthy = ` +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + generation: 1 + name: test + namespace: qual +status: + currentHealthy: 2 + desiredHealthy: 4 +` + +func TestPDBStatus(t *testing.T) { + testCases := map[string]testSpec{ + "pdbNoStatus": { + spec: pdbNoStatus, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "ZeroDesiredHealthy", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "pdbOK1": { + spec: pdbOK1, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "pdbMoreHealthy": { + spec: pdbMoreHealthy, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "pdbLessHealthy": { + spec: pdbLessHealthy, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "BudgetNotMet", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + } + + for tn, tc := range testCases { + tc := tc + t.Run(tn, func(t *testing.T) { + runStatusTest(t, tc) + }) + } +} + +var crdNoStatus = ` +apiVersion: something/v1 +kind: MyCR +metadata: + generation: 1 + name: test + namespace: qual +` + +var crdMismatchStatusGeneration = ` +apiVersion: something/v1 +kind: MyCR +metadata: + name: test + namespace: qual + generation: 2 +status: + observedGeneration: 1 +` + +var crdReady = ` +apiVersion: something/v1 +kind: MyCR +metadata: + name: test + namespace: qual + generation: 1 +status: + conditions: + - type: Ready + status: "True" + message: All looks ok + reason: AllOk +` + +var crdNotReady = ` +apiVersion: something/v1 +kind: MyCR +metadata: + generation: 1 + name: test + namespace: qual +status: + observedGeneration: 1 + conditions: + - type: Ready + status: "False" +` + +var crdNoCondition = ` +apiVersion: something/v1 +kind: MyCR +metadata: + name: test + namespace: qual + generation: 1 +status: + conditions: + - type: SomeCondition + status: "False" +` + +func TestCRDGenericStatus(t *testing.T) { + testCases := map[string]testSpec{ + "crdNoStatus": { + spec: crdNoStatus, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "crdReady": { + spec: crdReady, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "crdNotReady": { + spec: crdNotReady, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "crdNoCondition": { + spec: crdNoCondition, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "crdMismatchStatusGeneration": { + spec: crdMismatchStatusGeneration, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "LatestGenerationNotObserved", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + } + + for tn, tc := range testCases { + tc := tc + t.Run(tn, func(t *testing.T) { + runStatusTest(t, tc) + }) + } +} + +var jobNoStatus = ` +apiVersion: batch/v1 +kind: Job +metadata: + name: test + namespace: qual + generation: 1 +` + +var jobComplete = ` +apiVersion: batch/v1 +kind: Job +metadata: + name: test + namespace: qual + generation: 1 +status: + succeeded: 1 + active: 0 + conditions: + - type: Complete + status: "True" +` + +var jobFailed = ` +apiVersion: batch/v1 +kind: Job +metadata: + name: test + namespace: qual + generation: 1 +spec: + completions: 4 +status: + succeeded: 3 + failed: 1 + conditions: + - type: Failed + status: "True" + reason: JobFailed +` + +var jobInProgress = ` +apiVersion: batch/v1 +kind: Job +metadata: + name: test + namespace: qual + generation: 1 +spec: + completions: 10 + parallelism: 2 +status: + startTime: "2019-06-04T01:17:13Z" + succeeded: 3 + failed: 1 + active: 2 + conditions: + - type: Failed + status: "False" + - type: Complete + status: "False" +` + +func TestJobStatus(t *testing.T) { + testCases := map[string]testSpec{ + "jobNoStatus": { + spec: jobNoStatus, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "JobNotStarted", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "jobComplete": { + spec: jobComplete, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "jobFailed": { + spec: jobFailed, + expectedStatus: FailedStatus, + expectedConditions: []Condition{{ + Type: ConditionFailed, + Status: corev1.ConditionTrue, + Reason: "JobFailed", + }}, + absentConditionTypes: []ConditionType{ + ConditionInProgress, + }, + }, + "jobInProgress": { + spec: jobInProgress, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionInProgress, + ConditionFailed, + }, + }, + } + + for tn, tc := range testCases { + tc := tc + t.Run(tn, func(t *testing.T) { + runStatusTest(t, tc) + }) + } +} + +var cronjobNoStatus = ` +apiVersion: batch/v1 +kind: CronJob +metadata: + name: test + namespace: qual + generation: 1 +` + +var cronjobWithStatus = ` +apiVersion: batch/v1 +kind: CronJob +metadata: + name: test + namespace: qual + generation: 1 +status: +` + +func TestCronJobStatus(t *testing.T) { + testCases := map[string]testSpec{ + "cronjobNoStatus": { + spec: cronjobNoStatus, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "cronjobWithStatus": { + spec: cronjobWithStatus, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + } + + for tn, tc := range testCases { + tc := tc + t.Run(tn, func(t *testing.T) { + runStatusTest(t, tc) + }) + } +} + +var serviceDefault = ` +apiVersion: v1 +kind: Service +metadata: + name: test + namespace: qual + generation: 1 +` + +var serviceNodePort = ` +apiVersion: v1 +kind: Service +metadata: + name: test + namespace: qual + generation: 1 +spec: + type: NodePort +` + +var serviceLBok = ` +apiVersion: v1 +kind: Service +metadata: + name: test + namespace: qual + generation: 1 +spec: + type: LoadBalancer + clusterIP: "1.2.3.4" +` +var serviceLBnok = ` +apiVersion: v1 +kind: Service +metadata: + name: test + namespace: qual + generation: 1 +spec: + type: LoadBalancer +` + +func TestServiceStatus(t *testing.T) { + testCases := map[string]testSpec{ + "serviceDefault": { + spec: serviceDefault, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "serviceNodePort": { + spec: serviceNodePort, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + "serviceLBnok": { + spec: serviceLBnok, + expectedStatus: InProgressStatus, + expectedConditions: []Condition{{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: "NoIPAssigned", + }}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + }, + }, + "serviceLBok": { + spec: serviceLBok, + expectedStatus: CurrentStatus, + expectedConditions: []Condition{}, + absentConditionTypes: []ConditionType{ + ConditionFailed, + ConditionInProgress, + }, + }, + } + + for tn, tc := range testCases { + tc := tc + t.Run(tn, func(t *testing.T) { + runStatusTest(t, tc) + }) + } +} diff --git a/kstatus/status/util.go b/kstatus/status/util.go new file mode 100644 index 000000000..75ef8473d --- /dev/null +++ b/kstatus/status/util.go @@ -0,0 +1,112 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package status + +import ( + "strings" + + corev1 "sigs.k8s.io/kustomize/pseudo/k8s/api/core/v1" + apiunstructured "sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/runtime" +) + +// newInProgressCondition creates an inProgress condition with the given +// reason and message. +func newInProgressCondition(reason, message string) Condition { + return Condition{ + Type: ConditionInProgress, + Status: corev1.ConditionTrue, + Reason: reason, + Message: message, + } +} + +// newInProgressStatus creates a status Result with the InProgress status +// and an InProgress condition. +func newInProgressStatus(reason, message string) *Result { + return &Result{ + Status: InProgressStatus, + Message: message, + Conditions: []Condition{newInProgressCondition(reason, message)}, + } +} + +// ObjWithConditions Represent meta object with status.condition array +type ObjWithConditions struct { + // Status as expected to be present in most compliant kubernetes resources + Status ConditionStatus `json:"status" yaml:"status"` +} + +// ConditionStatus represent status with condition array +type ConditionStatus struct { + // Array of Conditions as expected to be present in kubernetes resources + Conditions []BasicCondition `json:"conditions" yaml:"conditions"` +} + +// BasicCondition fields that are expected in a condition +type BasicCondition struct { + // Type Condition type + Type string `json:"type" yaml:"type"` + // Status is one of True,False,Unknown + Status corev1.ConditionStatus `json:"status" yaml:"status"` + // Reason simple single word reason in CamleCase + // +optional + Reason string `json:"reason,omitempty" yaml:"reason"` + // Message human readable reason + // +optional + Message string `json:"message,omitempty" yaml:"message"` +} + +// GetObjectWithConditions return typed object +func GetObjectWithConditions(in map[string]interface{}) (*ObjWithConditions, error) { + var out = new(ObjWithConditions) + err := runtime.DefaultUnstructuredConverter.FromUnstructured(in, out) + if err != nil { + return nil, err + } + return out, nil +} + +// GetStringField return field as string defaulting to value if not found +func GetStringField(obj map[string]interface{}, fieldPath string, defaultValue string) string { + var rv = defaultValue + + fields := strings.Split(fieldPath, ".") + if fields[0] == "" { + fields = fields[1:] + } + + val, found, err := apiunstructured.NestedFieldNoCopy(obj, fields...) + if !found || err != nil { + return rv + } + + if v, ok := val.(string); ok { + return v + } + return rv +} + +// GetIntField return field as string defaulting to value if not found +func GetIntField(obj map[string]interface{}, fieldPath string, defaultValue int) int { + fields := strings.Split(fieldPath, ".") + if fields[0] == "" { + fields = fields[1:] + } + + val, found, err := apiunstructured.NestedFieldNoCopy(obj, fields...) + if !found || err != nil { + return defaultValue + } + + switch v := val.(type) { + case int: + return v + case int32: + return int(v) + case int64: + return int(v) + } + return defaultValue +} diff --git a/kstatus/status/util_test.go b/kstatus/status/util_test.go new file mode 100644 index 000000000..89333bf15 --- /dev/null +++ b/kstatus/status/util_test.go @@ -0,0 +1,59 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package status + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var testObj = map[string]interface{}{ + "f1": map[string]interface{}{ + "f2": map[string]interface{}{ + "i32": int32(32), + "i64": int64(64), + "float": 64.02, + "ms": []interface{}{ + map[string]interface{}{"f1f2ms0f1": 22}, + map[string]interface{}{"f1f2ms1f1": "index1"}, + }, + "msbad": []interface{}{ + map[string]interface{}{"f1f2ms0f1": 22}, + 32, + }, + }, + }, + + "ride": "dragon", + + "status": map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{"f1f2ms0f1": 22}, + map[string]interface{}{"f1f2ms1f1": "index1"}, + }, + }, +} + +func TestGetIntField(t *testing.T) { + v := GetIntField(testObj, ".f1.f2.i32", -1) + assert.Equal(t, int(32), v) + + v = GetIntField(testObj, ".f1.f2.wrongname", -1) + assert.Equal(t, int(-1), v) + + v = GetIntField(testObj, ".f1.f2.i64", -1) + assert.Equal(t, int(64), v) + + v = GetIntField(testObj, ".f1.f2.float", -1) + assert.Equal(t, int(-1), v) +} + +func TestGetStringField(t *testing.T) { + v := GetStringField(testObj, ".ride", "horse") + assert.Equal(t, v, "dragon") + + v = GetStringField(testObj, ".destination", "north") + assert.Equal(t, v, "north") +}