From eb44eb1d812d7ce5fe445cef9f80308bd67bc9cb Mon Sep 17 00:00:00 2001 From: Barni S Date: Tue, 4 Jun 2024 15:24:14 -0400 Subject: [PATCH] Support for Composition Validation - 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 --- .../api/v1alpha1/composition_types.go | 24 ++- .../api/v1alpha1/zz_generated.deepcopy.go | 22 +++ .../composition.google.com_compositions.yaml | 15 ++ .../controller/composition_controller.go | 174 +++++++++++++++++- 4 files changed, 233 insertions(+), 2 deletions(-) diff --git a/experiments/compositions/composition/api/v1alpha1/composition_types.go b/experiments/compositions/composition/api/v1alpha1/composition_types.go index 300657ec9b6..3ef4f008638 100644 --- a/experiments/compositions/composition/api/v1alpha1/composition_types.go +++ b/experiments/compositions/composition/api/v1alpha1/composition_types.go @@ -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 diff --git a/experiments/compositions/composition/api/v1alpha1/zz_generated.deepcopy.go b/experiments/compositions/composition/api/v1alpha1/zz_generated.deepcopy.go index 6de80a55e74..6f0876ba6ef 100644 --- a/experiments/compositions/composition/api/v1alpha1/zz_generated.deepcopy.go +++ b/experiments/compositions/composition/api/v1alpha1/zz_generated.deepcopy.go @@ -116,6 +116,13 @@ func (in *CompositionStatus) DeepCopyInto(out *CompositionStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Stages != nil { + in, out := &in.Stages, &out.Stages + *out = make(map[string]StageValidationStatus, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CompositionStatus. @@ -800,6 +807,21 @@ func (in *StageStatus) DeepCopy() *StageStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StageValidationStatus) DeepCopyInto(out *StageValidationStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StageValidationStatus. +func (in *StageValidationStatus) DeepCopy() *StageValidationStatus { + if in == nil { + return nil + } + out := new(StageValidationStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ValuesFrom) DeepCopyInto(out *ValuesFrom) { *out = *in diff --git a/experiments/compositions/composition/config/crd/bases/composition.google.com_compositions.yaml b/experiments/compositions/composition/config/crd/bases/composition.google.com_compositions.yaml index e20da3513c6..90971c523f8 100644 --- a/experiments/compositions/composition/config/crd/bases/composition.google.com_compositions.yaml +++ b/experiments/compositions/composition/config/crd/bases/composition.google.com_compositions.yaml @@ -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 diff --git a/experiments/compositions/composition/internal/controller/composition_controller.go b/experiments/compositions/composition/internal/controller/composition_controller.go index 9070978b801..cb0a584bc97 100644 --- a/experiments/compositions/composition/internal/controller/composition_controller.go +++ b/experiments/compositions/composition/internal/controller/composition_controller.go @@ -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" @@ -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") @@ -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 {