Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Ability for lint command to inspect referenced resources #2030

Merged
merged 8 commits into from
May 25, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ require (
google.golang.org/grpc v1.42.0
google.golang.org/protobuf v1.27.1
gopkg.in/yaml.v2 v2.4.0
istio.io/client-go v1.13.3
zachaller marked this conversation as resolved.
Show resolved Hide resolved
k8s.io/api v0.23.3
k8s.io/apiextensions-apiserver v0.23.1
k8s.io/apimachinery v0.23.3
Expand Down Expand Up @@ -162,6 +163,8 @@ require (
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
istio.io/api v0.0.0-20220413180505-1574de06b7bd // indirect
istio.io/gogo-genproto v0.0.0-20211208193508-5ab4acc9eb1e // indirect
k8s.io/cluster-bootstrap v0.0.0 // indirect
k8s.io/component-helpers v0.23.1 // indirect
k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1480,6 +1480,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
istio.io/api v0.0.0-20220413180505-1574de06b7bd h1:2i0e183JpTuZkZzqgXlv3mosCBxv7Bt9uUB4cNe7iSM=
istio.io/api v0.0.0-20220413180505-1574de06b7bd/go.mod h1:8ZZgyVgYrHhsFQarEgTfPnMGpdgTDZbxSjYhdwTUuAQ=
istio.io/client-go v1.13.3 h1:xbEgTX4NRlvVRI/JsCmMI0ATvCc9P85HkQ20SphEGZ4=
istio.io/client-go v1.13.3/go.mod h1:DeT/l4yO+bwyv0ZgavSTj7BfkA2cTckHD0jtluwtXhE=
istio.io/gogo-genproto v0.0.0-20211208193508-5ab4acc9eb1e h1:z2WI3y55w0K3c6hmarcp5EcOiP4vVpTBXA8nYstP+cE=
istio.io/gogo-genproto v0.0.0-20211208193508-5ab4acc9eb1e/go.mod h1:vJDAniIqryf/z///fgZqVPKJ7N2lBk7Gg8DCTB7oCfU=
k8s.io/api v0.23.1 h1:ncu/qfBfUoClqwkTGbeRqqOqBCRoUAflMuOaOD7J0c8=
k8s.io/api v0.23.1/go.mod h1:WfXnOnwSqNtG62Y1CdjoMxh7r7u9QXGCkA1u0na2jgo=
k8s.io/apiextensions-apiserver v0.23.1 h1:xxE0q1vLOVZiWORu1KwNRQFsGWtImueOrqSl13sS5EU=
Expand Down
1 change: 0 additions & 1 deletion pkg/apis/rollouts/validation/validation_references.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ func ValidateService(svc ServiceWithType, rollout *v1alpha1.Rollout) field.Error
}
if v, ok := rollout.Spec.Template.Labels[svcLabelKey]; !ok || v != svcLabelValue {
msg := fmt.Sprintf("Service %q has unmatch lable %q in rollout", service.Name, svcLabelKey)
fmt.Println(msg)
allErrs = append(allErrs, field.Invalid(fldPath, service.Name, msg))
}
}
Expand Down
283 changes: 241 additions & 42 deletions pkg/kubectl-argo-rollouts/cmd/lint/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,39 @@ package lint

import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"unicode"

"github.com/argoproj/argo-rollouts/pkg/apis/rollouts"
"github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
"github.com/argoproj/argo-rollouts/pkg/apis/rollouts/validation"
"github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/options"
ingressutil "github.com/argoproj/argo-rollouts/utils/ingress"
"github.com/ghodss/yaml"
"github.com/spf13/cobra"
goyaml "gopkg.in/yaml.v2"
istioNetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3"
istioNetworkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
v1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
)

type LintOptions struct {
options.ArgoRolloutsOptions
File string
}

type roAndReferences struct {
Rollout v1alpha1.Rollout
References validation.ReferencedResources
}

const (
lintExample = `
# Lint a rollout
Expand Down Expand Up @@ -56,56 +69,19 @@ func NewCmdLint(o *options.ArgoRolloutsOptions) *cobra.Command {
return cmd
}

// isJSON detects if the byte array looks like json, based on the first non-whitespace character
func isJSON(fileBytes []byte) bool {
for _, b := range fileBytes {
if !unicode.IsSpace(rune(b)) {
return b == '{'
}
}
return false
}

func unmarshal(fileBytes []byte, obj interface{}) error {
if isJSON(fileBytes) {
decoder := json.NewDecoder(bytes.NewReader(fileBytes))
decoder.DisallowUnknownFields()
return decoder.Decode(&obj)
}
return yaml.UnmarshalStrict(fileBytes, &obj, yaml.DisallowUnknownFields)
}

func validate(fileBytes []byte, un *unstructured.Unstructured) error {
gvk := un.GroupVersionKind()
switch {
case gvk.Group == rollouts.Group && gvk.Kind == rollouts.RolloutKind:
var ro v1alpha1.Rollout
err := unmarshal(fileBytes, &ro)
if err != nil {
return err
}
errs := validation.ValidateRollout(&ro)
if 0 < len(errs) {
return errs[0]
}
}
return nil
}

func (l *LintOptions) lintResource(path string) error {
fileBytes, err := ioutil.ReadFile(path)
if err != nil {
return err
}

var un unstructured.Unstructured

if isJSON(fileBytes) {
if err = unmarshal(fileBytes, un); err != nil {
return err
}
return validate(fileBytes, &un)
}
var refResource validation.ReferencedResources
var fileRollouts []v1alpha1.Rollout

decoder := goyaml.NewDecoder(bytes.NewReader(fileBytes))
for {
Expand All @@ -128,10 +104,233 @@ func (l *LintOptions) lintResource(path string) error {
return err
}

if err = validate(valueBytes, &un); err != nil {
gvk := un.GroupVersionKind()
if gvk.Group == rollouts.Group && gvk.Kind == rollouts.RolloutKind {
var ro v1alpha1.Rollout
err := unmarshal(valueBytes, &ro)
if err != nil {
return err
}
fileRollouts = append(fileRollouts, ro)
}
err = buildAllReferencedResources(gvk, valueBytes, &refResource)
if err != nil {
return err
}
}

setServiceTypeAndManagedAnnotation(fileRollouts, refResource)
setIngressManagedAnnotation(fileRollouts, refResource)
setVirtualServiceManagedAnnotation(fileRollouts, refResource)

var errList field.ErrorList
for _, rollout := range fileRollouts {
roRef := matchRolloutToReferences(rollout, refResource)

errList = append(errList, validation.ValidateRollout(&roRef.Rollout)...)
errList = append(errList, validation.ValidateRolloutReferencedResources(&roRef.Rollout, roRef.References)...)
}

for _, e := range errList {
fmt.Println(e.ErrorBody())
}
if len(errList) > 0 {
return errList[0]
} else {
return nil
}
}

// buildAllReferencedResources This builds a ReferencedResources object that has all the external resources for every
// rollout resource in the manifest. We will need to later match each referenced resource to its own rollout resource
// before passing the rollout object and its managed reference on to validation.
func buildAllReferencedResources(gvk schema.GroupVersionKind, valueBytes []byte, refResource *validation.ReferencedResources) error {
switch {
case gvk.Group == v1.GroupName && gvk.Kind == "Service":
var svc v1.Service
err := unmarshal(valueBytes, &svc)
if err != nil {
return err
}
refResource.ServiceWithType = append(refResource.ServiceWithType, validation.ServiceWithType{
Service: &svc,
})

case gvk.Group == istioNetworkingv1beta1.GroupName && gvk.Kind == "VirtualService":
var virtualServicev1beta1 istioNetworkingv1beta1.VirtualService
var virtualServicev1alpha3 istioNetworkingv1alpha3.VirtualService
if gvk.Version == "v1alpha3" {
err := unmarshal(valueBytes, &virtualServicev1alpha3)
if err != nil {
return err
}

vsvcUn, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&virtualServicev1alpha3)
if err != nil {
return err
}
refResource.VirtualServices = append(refResource.VirtualServices, unstructured.Unstructured{
Object: vsvcUn,
})
} else if gvk.Version == "v1beta1" {
err := unmarshal(valueBytes, &virtualServicev1beta1)
if err != nil {
return err
}
vsvcUn, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&virtualServicev1beta1)
if err != nil {
return err
}
refResource.VirtualServices = append(refResource.VirtualServices, unstructured.Unstructured{
Object: vsvcUn,
})
}

case (gvk.Group == networkingv1.GroupName || gvk.Group == extensionsv1beta1.GroupName) && gvk.Kind == "Ingress":
var ing networkingv1.Ingress
var ingv1beta1 extensionsv1beta1.Ingress
if gvk.Version == "v1" {
err := unmarshal(valueBytes, &ing)
if err != nil {
return err
}
refResource.Ingresses = append(refResource.Ingresses, *ingressutil.NewIngress(&ing))
} else if gvk.Version == "v1beta1" {
err := unmarshal(valueBytes, &ingv1beta1)
if err != nil {
return err
}
refResource.Ingresses = append(refResource.Ingresses, *ingressutil.NewLegacyIngress(&ingv1beta1))
}

}
return nil
}

// matchRolloutToReferences This function goes through the global list of all ReferencedResources in the manifest and matches
// them up with their respective rollout object so that we can latter have a mapping of a single rollout object and its
// referenced resources.
func matchRolloutToReferences(rollout v1alpha1.Rollout, refResource validation.ReferencedResources) roAndReferences {
matchedReferenceResources := roAndReferences{Rollout: rollout, References: validation.ReferencedResources{}}

for _, service := range refResource.ServiceWithType {
if service.Service.Annotations[v1alpha1.ManagedByRolloutsKey] == rollout.Name {
matchedReferenceResources.References.ServiceWithType = append(matchedReferenceResources.References.ServiceWithType, service)
}
}
for _, ingress := range refResource.Ingresses {
if ingress.GetAnnotations()[v1alpha1.ManagedByRolloutsKey] == rollout.Name {
matchedReferenceResources.References.Ingresses = append(matchedReferenceResources.References.Ingresses, ingress)
}
}
for _, virtualService := range refResource.VirtualServices {
if virtualService.GetAnnotations()[v1alpha1.ManagedByRolloutsKey] == rollout.Name {
matchedReferenceResources.References.VirtualServices = append(matchedReferenceResources.References.VirtualServices, virtualService)
}
}

return matchedReferenceResources
}

// setServiceTypeAndManagedAnnotation This sets the managed annotation on each service as well as figures out what
// type of service its is by looking at the rollout and set's its service type accordingly.
func setServiceTypeAndManagedAnnotation(rollouts []v1alpha1.Rollout, refResource validation.ReferencedResources) {
for _, rollout := range rollouts {
for i := range refResource.ServiceWithType {

if refResource.ServiceWithType[i].Service.Annotations == nil {
refResource.ServiceWithType[i].Service.Annotations = make(map[string]string)
}

if rollout.Spec.Strategy.Canary != nil {
if rollout.Spec.Strategy.Canary.CanaryService == refResource.ServiceWithType[i].Service.Name {
refResource.ServiceWithType[i].Type = validation.CanaryService
refResource.ServiceWithType[i].Service.Annotations[v1alpha1.ManagedByRolloutsKey] = rollout.Name
}
if rollout.Spec.Strategy.Canary.StableService == refResource.ServiceWithType[i].Service.Name {
refResource.ServiceWithType[i].Type = validation.StableService
refResource.ServiceWithType[i].Service.Annotations[v1alpha1.ManagedByRolloutsKey] = rollout.Name
}
if rollout.Spec.Strategy.Canary.PingPong != nil {
if rollout.Spec.Strategy.Canary.PingPong.PingService == refResource.ServiceWithType[i].Service.Name {
refResource.ServiceWithType[i].Type = validation.PingService
refResource.ServiceWithType[i].Service.Annotations[v1alpha1.ManagedByRolloutsKey] = rollout.Name
}
if rollout.Spec.Strategy.Canary.PingPong.PongService == refResource.ServiceWithType[i].Service.Name {
refResource.ServiceWithType[i].Type = validation.PongService
refResource.ServiceWithType[i].Service.Annotations[v1alpha1.ManagedByRolloutsKey] = rollout.Name
}
}
}

if rollout.Spec.Strategy.BlueGreen != nil {
if rollout.Spec.Strategy.BlueGreen.ActiveService == refResource.ServiceWithType[i].Service.Name {
refResource.ServiceWithType[i].Type = validation.ActiveService
refResource.ServiceWithType[i].Service.Annotations[v1alpha1.ManagedByRolloutsKey] = rollout.Name
}
if rollout.Spec.Strategy.BlueGreen.PreviewService == refResource.ServiceWithType[i].Service.Name {
refResource.ServiceWithType[i].Type = validation.PreviewService
refResource.ServiceWithType[i].Service.Annotations[v1alpha1.ManagedByRolloutsKey] = rollout.Name
}
}

}
}
}

// setIngressManagedAnnotation This tries to find ingresses that have matching services in the rollout resource and if so
// it will add the managed by annotations just for linting so that we can later match up resources to a rollout resources
// for the case when we have multiple rollout resources in a single manifest.
func setIngressManagedAnnotation(rollouts []v1alpha1.Rollout, refResource validation.ReferencedResources) {
for _, rollout := range rollouts {
for i := range refResource.Ingresses {
var serviceName string
if rollout.Spec.Strategy.Canary.TrafficRouting.Nginx != nil {
serviceName = rollout.Spec.Strategy.Canary.StableService
} else if rollout.Spec.Strategy.Canary.TrafficRouting.ALB != nil {
serviceName = rollout.Spec.Strategy.Canary.StableService
if rollout.Spec.Strategy.Canary.TrafficRouting.ALB.RootService != "" {
serviceName = rollout.Spec.Strategy.Canary.TrafficRouting.ALB.RootService
}
} else if rollout.Spec.Strategy.Canary.TrafficRouting.SMI != nil {
serviceName = rollout.Spec.Strategy.Canary.TrafficRouting.SMI.RootService
}

if ingressutil.HasRuleWithService(&refResource.Ingresses[i], serviceName) {
annotations := refResource.Ingresses[i].GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations[v1alpha1.ManagedByRolloutsKey] = rollout.Name
refResource.Ingresses[i].SetAnnotations(annotations)
}
}
}
}

// setVirtualServiceManagedAnnotation This function finds virtual services that are listed in the rollout resources and
// adds the ManagedByRolloutsKey to the annotations of the virtual services.
func setVirtualServiceManagedAnnotation(ro []v1alpha1.Rollout, refResource validation.ReferencedResources) {
for _, rollout := range ro {
for i := range refResource.VirtualServices {
if rollout.Spec.Strategy.Canary.TrafficRouting.Istio.VirtualService != nil && rollout.Spec.Strategy.Canary.TrafficRouting.Istio.VirtualService.Name == refResource.VirtualServices[i].GetName() {
annotations := refResource.VirtualServices[i].GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations[v1alpha1.ManagedByRolloutsKey] = rollout.Name
refResource.VirtualServices[i].SetAnnotations(annotations)
}
for _, virtualService := range rollout.Spec.Strategy.Canary.TrafficRouting.Istio.VirtualServices {
if virtualService.Name == refResource.VirtualServices[i].GetName() {
annotations := refResource.VirtualServices[i].GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations[v1alpha1.ManagedByRolloutsKey] = rollout.Name
refResource.VirtualServices[i].SetAnnotations(annotations)
}
}
}
}
}
Loading