Skip to content

Commit

Permalink
feat: Ability for lint command to inspect referenced resources (#2030)
Browse files Browse the repository at this point in the history
Signed-off-by: zachaller <[email protected]>
  • Loading branch information
zachaller authored May 25, 2022
1 parent c757147 commit 15df713
Show file tree
Hide file tree
Showing 17 changed files with 1,773 additions and 62 deletions.
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
259 changes: 217 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,35 @@ 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"
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/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 +65,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 +100,213 @@ 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(un, &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(un unstructured.Unstructured, refResource *validation.ReferencedResources) error {

valueBytes, err := un.MarshalJSON()
if err != nil {
return err
}

gvk := un.GroupVersionKind()
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 == "networking.istio.io" && gvk.Kind == "VirtualService":
refResource.VirtualServices = append(refResource.VirtualServices, un)

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

0 comments on commit 15df713

Please sign in to comment.