Skip to content

Commit

Permalink
Add readiness check for resources
Browse files Browse the repository at this point in the history
This feature ensures users are allowed to define when a
resource is ready or not.

For now this feature only allows to look within itself to see
whether it's ready or not, so only its spec and status can be
accessed using CEL fields.

An example with deployement-service RG will be included in this PR
  • Loading branch information
michaelhtm committed Oct 16, 2024
1 parent 465b437 commit f08a3c6
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 9 deletions.
2 changes: 2 additions & 0 deletions api/v1alpha1/resource_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ type Resource struct {
Name string `json:"name,omitempty"`
// +kubebuilder:validation:Required
Definition runtime.RawExtension `json:"definition,omitempty"`
// +kubebuilder:valisation:Optional
ReadyOn []string `json:"readyOn,omitempty"`
}

// ResourceGroupStatus defines the observed state of ResourceGroup
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

5 changes: 5 additions & 0 deletions config/crd/bases/x.symphony.k8s.aws_resourcegroups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ spec:
x-kubernetes-preserve-unknown-fields: true
name:
type: string
readyOn:
items:
type: string
x-kubernetes-preserve-unknown-fields: true
type: array
required:
- definition
- name
Expand Down
4 changes: 3 additions & 1 deletion examples/deployment-service/deployment-service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ spec:
availableReplicas: ${deployment.status.availableReplicas}
resources:
- name: deployment
readyOn:
- ${spec.replicas == status.availableReplicas}
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${spec.name}
spec:
replicas: 1
replicas: 50
selector:
matchLabels:
app: deployment
Expand Down
4 changes: 4 additions & 0 deletions internal/celutil/conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ func ConvertCELtoGo(v ref.Val) (interface{}, error) {
return v.Value(), fmt.Errorf("unsupported type: %v", v.Type())
}
}

func IsBoolType(v ref.Val) bool {
return v.Type() == types.BoolType
}
7 changes: 7 additions & 0 deletions internal/controller/instance/controller_reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ func (igr *InstanceGraphReconciler) reconcileResource(ctx context.Context, resou
return resourceState.Err
}

log.V(1).Info("Checking if resource is Ready", "resource", resourceID)
if !igr.runtime.IsResourceReady(resourceMeta.ReadyOnExpressions, observed) {
log.V(1).Info("Resource not ready", "resource", resourceID)
resourceState.State = "CREATING"
resourceState.Err = fmt.Errorf("resource not ready")
return igr.delayedRequeue(resourceState.Err)
}
igr.runtime.SetLatestResource(resourceID, observed)
return igr.updateResource(ctx, rc, rUnstructured, observed, resourceID, resourceState)
}
Expand Down
65 changes: 57 additions & 8 deletions internal/resourcegroup/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,17 +174,28 @@ func (b *GraphBuilder) NewResourceGroup(rg *v1alpha1.ResourceGroup) (*ResourceGr
})
}
}
// 6. Parse ReadyOn expressions
readyOn, err := parser.ParseReadyOn(rgResource.ReadyOn)
if err != nil {
return nil, fmt.Errorf("failed to parse readyOn expressions: %v", err)
}

readyOnExpressions := []ReadyOnExpression{}
for _, e := range readyOn {
readyOnExpressions = append(readyOnExpressions, ReadyOnExpression{Resolved: false, Expression: e})
}

_, isNamespaced := namespacedResources[gvk]

resources[rgResource.Name] = &Resource{
ID: rgResource.Name,
GroupVersionKind: gvk,
Schema: resourceSchema,
EmulatedObject: emulatedResource,
OriginalObject: unstructuredResource,
Variables: resourceVariables,
Namespaced: isNamespaced,
ID: rgResource.Name,
GroupVersionKind: gvk,
Schema: resourceSchema,
EmulatedObject: emulatedResource,
OriginalObject: unstructuredResource,
Variables: resourceVariables,
ReadyOnExpressions: readyOnExpressions,
Namespaced: isNamespaced,
}
}

Expand Down Expand Up @@ -335,6 +346,44 @@ func (b *GraphBuilder) validateResourceCELExpressions(resources map[string]*Reso
return fmt.Errorf("failed to dry-run expression %s: %w", expression, err)
}
}
// validate readyOn Expressions for resource
// Only accepting expressions accessing the status and spec for now
// and need to evaluate to a boolean type
for _, expression := range resource.ReadyOnExpressions {
resourceNames := []string{"status", "spec"}
env, err := celutil.NewEnvironement(celutil.WithResourceNames(resourceNames))
if err != nil {
return fmt.Errorf("failed to create CEL environment: %w", err)
}
err = b.validateCELExpressionContext(env, expression.Expression, resourceNames)
if err != nil {
return fmt.Errorf("failed to validate expression context: '%s' %w", expression.Expression, err)
}

// create context
// context := map[string]*Resource{}
// add resource status to the context
context := map[string]*Resource{}
for _, n := range resourceNames {
context[n] = &Resource{
ID: n,
GroupVersionKind: resource.GroupVersionKind,
Schema: resource.Schema,
EmulatedObject: &unstructured.Unstructured{
Object: resource.EmulatedObject.Object[n].(map[string]interface{}),
},
}
}

output, err := b.dryRunExpression(env, expression.Expression, context)

if err != nil {
return fmt.Errorf("failed to dry-run expression %s: %w", expression.Expression, err)
}
if !celutil.IsBoolType(output) {
return fmt.Errorf("output of readyOn expression %s can only be of type bool", expression.Expression)
}
}
}
}

Expand Down Expand Up @@ -501,7 +550,7 @@ func (b *GraphBuilder) buildInstanceResource(
instanceVariables := []*ResourceVariable{}
for _, statusVariable := range statusVariables {
// These variables needs to be injected into the status field of the instance.
path := ".status" + statusVariable.Path
path := ">status" + statusVariable.Path
statusVariable.Path = path
instanceVariables = append(instanceVariables, &ResourceVariable{
Kind: ResourceVariableKindDynamic,
Expand Down
19 changes: 19 additions & 0 deletions internal/resourcegroup/ready_on_expression.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package resourcegroup

type ReadyOnExpression struct {
Resolved bool
Expression string
}
2 changes: 2 additions & 0 deletions internal/resourcegroup/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type Resource struct {

Variables []*ResourceVariable

ReadyOnExpressions []ReadyOnExpression

Dependencies []string
Namespaced bool
}
Expand Down
54 changes: 54 additions & 0 deletions internal/resourcegroup/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,60 @@ func (rt *RuntimeResourceGroup) CanResolveResource(resource string) bool {
return false
}
}

return true
}

func (rt *RuntimeResourceGroup) IsResourceReady(expressions []ReadyOnExpression, resource *unstructured.Unstructured) bool {

if len(expressions) == 0 {
return true
}

resourceNames := []string{"status", "spec"}
env, err := celutil.NewEnvironement(&celutil.EnvironementOptions{
ResourceNames: resourceNames,
})
context := map[string]interface{}{}

for _, n := range resourceNames {
if obj, ok := resource.Object[n]; ok {
context[n] = obj.(map[string]interface{})
}
}
// we should not expect errors here since we already compiled it
// in the dryRun
if err != nil {
return false
}
for _, e := range expressions {
if e.Resolved {
continue
}
ast, issues := env.Compile(e.Expression)
if issues != nil && issues.Err() != nil {
return false
}
fmt.Println(e.Expression)
program, err := env.Program(ast)
if err != nil {
return false
}

output, _, err := program.Eval(context)
if err != nil {
return false
}
out, err := celutil.ConvertCELtoGo(output)
if err != nil {
return false
}
// keep checking for a false
if !out.(bool) {
return false
}
e.Resolved = out.(bool)
}
return true
}

Expand Down
18 changes: 18 additions & 0 deletions internal/typesystem/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ func ParseResource(resource map[string]interface{}, resourceSchema *spec.Schema)
return parseResource(resource, resourceSchema, "")
}

func ParseReadyOn(readyOn []string) ([]string, error) {

expressions := make([]string, 0, len(readyOn))

for _, e := range readyOn {
ok, err := isOneShotExpression(e)
if err != nil {
return nil, err
}
if !ok {
return nil, fmt.Errorf("single expression per line allowed")
}
expressions = append(expressions, strings.Trim(e, "${}"))
}

return expressions, nil
}

// parseResource is a helper function that recursively extracts CEL expressions
// from a resource. It uses a depthh first search to traverse the resource and
// extract expressions from string fields
Expand Down

0 comments on commit f08a3c6

Please sign in to comment.