Skip to content

Commit

Permalink
Support for Composition Validation
Browse files Browse the repository at this point in the history
- Add fields in composition status
  - record last generation seen
  - add status map for each expander capturing validation result
- Add logic to callout the grpc.validate method for each expander
  • Loading branch information
barney-s committed Jun 4, 2024
1 parent ea7ee5c commit eb44eb1
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,31 @@ type CompositionSpec struct {
NamespaceMode NamespaceMode `json:"namespaceMode,omitempty"`
}

type ValidationStatus string

const (
// ValidationStatusUnkown is when it is not validated
ValidationStatusUnknown ValidationStatus = "unknown"
// ValidationStatusSuccess is when valdiation succeeds
ValidationStatusSuccess ValidationStatus = "success"
// ValidationStatusFailed is when valdiation fails
ValidationStatusFailed ValidationStatus = "failed"
// ValidationStatusError is when validation was not called
ValidationStatusError ValidationStatus = "error"
)

// StageStatus captures the status of a stage
type StageValidationStatus struct {
ValidationStatus ValidationStatus `json:"validationStatus,omitempty"`
Reason string `json:"reason,omitempty"`
Message string `json:"message,omitempty"`
}

// CompositionStatus defines the observed state of Composition
type CompositionStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`
Generation int64 `json:"generation,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
Stages map[string]StageValidationStatus `json:"stages,omitempty"`
}

//+kubebuilder:object:root=true
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,21 @@ spec:
- type
type: object
type: array
generation:
format: int64
type: integer
stages:
additionalProperties:
description: StageStatus captures the status of a stage
properties:
message:
type: string
reason:
type: string
validationStatus:
type: string
type: object
type: object
type: object
type: object
served: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ package controller

import (
"context"
"encoding/json"
"fmt"
"reflect"
"strings"
"sync"

"github.com/go-logr/logr"
compositionv1alpha1 "google.com/composition/api/v1alpha1"
pb "google.com/composition/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -103,13 +108,19 @@ func (r *CompositionReconciler) Reconcile(ctx context.Context, req ctrl.Request)

logger = logger.WithName(composition.Name).WithName(fmt.Sprintf("%d", composition.Generation))

composition.Status.ClearCondition(compositionv1alpha1.Error)
logger.Info("Validating Compostion object")
if !composition.Validate() {
logger.Info("Validation Failed")
return ctrl.Result{}, fmt.Errorf("Validation failed")
}

composition.Status.ClearCondition(compositionv1alpha1.Error)
logger.Info("Validating expander configs")
if err := r.validateExpanders(ctx, logger, &composition); err != nil {
logger.Info("expander config validation failed")
return ctrl.Result{}, err
}

logger.Info("Processing Composition object")
if err := r.processComposition(ctx, &composition, logger); err != nil {
logger.Info("Error processing Composition")
Expand All @@ -119,6 +130,167 @@ func (r *CompositionReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, nil
}

func (r *CompositionReconciler) validateExpanders(
ctx context.Context, logger logr.Logger, c *compositionv1alpha1.Composition,
) error {
errorStages := []string{}
if c.Status.Stages == nil {
c.Status.Stages = make(map[string]compositionv1alpha1.StageValidationStatus)
}
for _, expander := range c.Spec.Expanders {
uri, ev, reason, err := r.getExpanderValue(ctx, expander.Version, expander.Type)
if err != nil {
logger.Error(err, "Error getting ExpanderVersion", "type", expander.Type, "version", expander.Version)
errorStages = append(errorStages, expander.Name)
c.Status.Stages[expander.Name] = compositionv1alpha1.StageValidationStatus{
ValidationStatus: compositionv1alpha1.ValidationStatusError,
Reason: reason,
Message: err.Error(),
}
// Try the next expander
continue
}
logger.Info("Got valid expander uri", "uri", uri)

// We dont have validate for Job type expander
if ev.Spec.Type != compositionv1alpha1.ExpanderTypeGRPC {
c.Status.Stages[expander.Name] = compositionv1alpha1.StageValidationStatus{
ValidationStatus: compositionv1alpha1.ValidationStatusUnknown,
Message: "expander type does not implement validation",
Reason: "NoValidationSupport",
}
} else {
reason, err := r.validateExpanderConfig(ctx, logger, expander, ev, uri)
if err != nil {
logger.Error(err, "Validating config failed", "type", expander.Type, "version", expander.Version)
errorStages = append(errorStages, expander.Name)
c.Status.Stages[expander.Name] = compositionv1alpha1.StageValidationStatus{
ValidationStatus: compositionv1alpha1.ValidationStatusFailed,
Reason: reason,
Message: err.Error(),
}
// Try the next expander
continue
}

c.Status.Stages[expander.Name] = compositionv1alpha1.StageValidationStatus{
ValidationStatus: compositionv1alpha1.ValidationStatusSuccess,
Reason: "ValidationPassed",
Message: "",
}
}
}

if len(errorStages) != 0 {
message := fmt.Sprintf("Validating failed for stages: %s", strings.Join(errorStages, ", "))
c.Status.Conditions = append(c.Status.Conditions, metav1.Condition{
LastTransitionTime: metav1.Now(),
Message: message,
Reason: "ValidationFailed",
Type: string(compositionv1alpha1.Error),
Status: metav1.ConditionTrue,
})
r.Recorder.Event(c, "Warning", "ValidationFailed", message)
return fmt.Errorf("Validation Failed")
}

return nil
}
func (r *CompositionReconciler) validateExpanderConfig(ctx context.Context, logger logr.Logger,
expander compositionv1alpha1.Expander, ev *compositionv1alpha1.ExpanderVersion, grpcService string) (string, error) {
// Set up a connection to the server.
conn, err := grpc.Dial(grpcService, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
logger.Error(err, "grpc dial failed: "+grpcService)
return "GRPCConnError", err
}

// marshall expander config
// read Expander config from in cr.namespace
var configBytes []byte
if expander.Reference != nil {
expanderconfigcr := unstructured.Unstructured{}
expanderconfigcr.SetGroupVersionKind(schema.GroupVersionKind{
Group: ev.Spec.Config.Group,
Version: ev.Spec.Config.Version,
Kind: ev.Spec.Config.Kind,
})
expanderconfigNN := types.NamespacedName{Namespace: expander.Reference.Namespace, Name: expander.Reference.Name}
if err := r.Get(ctx, expanderconfigNN, &expanderconfigcr); err != nil {
logger.Error(err, "unable to fetch ExpanderConfig CR", "expander config", expanderconfigNN)
return "GetExpanderConfigFailed", err
}
configBytes, err = json.Marshal(expanderconfigcr.Object)
if err != nil {
logger.Error(err, "failed to marshal ExpanderConfig Object")
return "MarshallExpanderConfigFailed", err
}
} else {
// TODO check if json.Marshall is escaping quotes
// Also causes > to be replaced unicode 'if loop.index \u003e 1'
err = nil
//configBytes, err = json.Marshal(expander.Template)
configBytes = []byte(expander.Template)
if err != nil {
logger.Error(err, "failed to marshall Expander template")
return "MarshallExpanderTemplateFailed", err
}
}

expanderClient := pb.NewExpanderClient(conn)
result, err := expanderClient.Validate(ctx,
&pb.ValidateRequest{
Config: configBytes,
})
if err != nil {
logger.Error(err, "expander.Validate() Failed", "expander", expander.Name)
return "ValidateError", err
}
if result.Status != pb.Status_SUCCESS {
logger.Error(nil, "expander.Validate() Status is not Success", "expander",
expander.Name, "status", result.Status, "message", result.Error.Message)
err = fmt.Errorf("Validate Failed: %s", result.Error.Message)
return "ValidateStatusFailed", err
}

return "", nil
}

func (r *CompositionReconciler) getExpanderValue(
ctx context.Context, inputExpanderVersion string, expanderType string,
) (string, *compositionv1alpha1.ExpanderVersion, string, error) {
logger := log.FromContext(ctx)

value := ""
var ev compositionv1alpha1.ExpanderVersion
err := r.Client.Get(ctx,
types.NamespacedName{
Name: "composition-" + expanderType,
Namespace: "composition-system"},
&ev)

if err != nil {
// The CR should be created before the specified expander can be used.
logger.Error(err, "Failed to get the ExpanderVersionCR")
if apierrors.IsNotFound(err) {
return value, nil, "MissingExpanderCR", err
} else {
return value, nil, "ErrorGettingExpanderVersionCR", err
}
}

if ev.Status.VersionMap == nil {
return value, nil, "ErrorEmptyVersionMap", fmt.Errorf("ExpanderVersion .status.versionMap is empty")
}

logger.Info("input expander version", "current", inputExpanderVersion)
value, ok := ev.Status.VersionMap[inputExpanderVersion]
if !ok {
return value, nil, "VersionNotFound", fmt.Errorf("%s version not found", inputExpanderVersion)
}
return value, &ev, "", nil
}

func (r *CompositionReconciler) processComposition(
ctx context.Context, c *compositionv1alpha1.Composition, logger logr.Logger,
) error {
Expand Down

0 comments on commit eb44eb1

Please sign in to comment.