Skip to content

Commit

Permalink
Include Schema in runtime resource
Browse files Browse the repository at this point in the history
The schema is necessary to get the top level fields of a resource,
which is necessary when running readyOn expressions. In the future
we can store the top level fields and remove the schema if needed.

Also adding a new resource variable kind for readyOn expressions,
to prevent the resolveDynamicVariables function from evaluating them.
  • Loading branch information
michaelhtm committed Oct 25, 2024
1 parent 22da6cf commit 71a1183
Show file tree
Hide file tree
Showing 5 changed files with 35 additions and 22 deletions.
9 changes: 2 additions & 7 deletions internal/controller/instance/controller_reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,16 +198,11 @@ func (igr *instanceGraphReconciler) reconcileResource(ctx context.Context, resou
igr.runtime.SetResource(resourceID, observed)

log.V(1).Info("Checking if resource is Ready", "resource", resourceID)
if ready, err := igr.runtime.IsResourceReady(resourceID); err != nil {
log.V(1).Info("Resource not ready", "resource", resourceID)
if ready, reason, err := igr.runtime.IsResourceReady(resourceID); err != nil || !ready {
log.V(1).Info("Resource not ready", "resource", resourceID, "reason", reason, "error", err)
resourceState.State = "WaitingForReadiness"
resourceState.Err = fmt.Errorf("resource not ready: %w", err)
return igr.delayedRequeue(resourceState.Err)
} else if !ready {
log.V(1).Info("Resource not ready", "resource", resourceID)
resourceState.State = "WaitingForReadiness"
resourceState.Err = fmt.Errorf("resource not ready")
return igr.delayedRequeue(resourceState.Err)
}

return igr.updateResource(ctx, rc, rUnstructured, observed, resourceID, resourceState)
Expand Down
1 change: 1 addition & 0 deletions internal/resourcegroup/graph/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ func (r *Resource) DeepCopy() *Resource {
return &Resource{
id: r.id,
gvr: r.gvr,
schema: r.schema,
originalObject: r.originalObject.DeepCopy(),
variables: slices.Clone(r.variables),
dependencies: slices.Clone(r.dependencies),
Expand Down
34 changes: 20 additions & 14 deletions internal/resourcegroup/runtime/resourcegroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func NewResourceGroupRuntime(
for _, expr := range resource.GetReadyOnExpressions() {
ees := &expressionEvaluationState{
Expression: expr,
Kind: variable.ResourceVariableKindDynamic,
Kind: variable.ResourceVariableKindReadyOn,
}
r.expressionsCache[expr] = ees
}
Expand Down Expand Up @@ -473,28 +473,27 @@ func (rt *ResourceGroupRuntime) allExpressionsAreResolved() bool {
// IsResourceReady checks if a resource is ready based on the readyOnExpressions
// defined in the resource. If no readyOnExpressions are defined, the resource
// is considered ready.
func (rt *ResourceGroupRuntime) IsResourceReady(resourceID string) (bool, error) {
func (rt *ResourceGroupRuntime) IsResourceReady(resourceID string) (bool, string, error) {
observed, ok := rt.resolvedResources[resourceID]
if !ok {
// Users need to make sure that the resource is resolved a.k.a (SetResource)
// before calling this function.
return false, fmt.Errorf("resource %s not found", resourceID)
return false, fmt.Sprintf("resource %s is not created", resourceID), nil
}

expressions := rt.resources[resourceID].GetReadyOnExpressions()
if len(expressions) == 0 {
return true, nil
return true, "", nil
}

topLevelFields := rt.resources[resourceID].GetTopLevelFields()
env, err := celutil.NewEnvironement(&celutil.EnvironementOptions{
ResourceNames: topLevelFields,
})

// we should not expect errors here since we already compiled it
// in the dryRun
if err != nil {
return false, fmt.Errorf("failed creating new Environment: %w", err)
return false, "", fmt.Errorf("failed creating new Environment: %w", err)
}
context := map[string]interface{}{}
for _, n := range topLevelFields {
Expand All @@ -507,29 +506,36 @@ func (rt *ResourceGroupRuntime) IsResourceReady(resourceID string) (bool, error)
// the result. NOTE(a-hilaly): maybe we can cache the result, but for that
// we also need to define a new Kind for the variables, they are not dynamic
// nor static. And for sure they need to be expressionEvaluationStateo objects.
//
// We shouldn't expect an error here, since we tested it with dry run
// keeping check just in case
ast, issues := env.Compile(expression)
if issues != nil && issues.Err() != nil {
return false, fmt.Errorf("failed compiling expression %s: %w", expression, err)
return false, "", fmt.Errorf("failed compiling expression %s: %w", expression, issues.Err())
}
// Here as well
program, err := env.Program(ast)
if err != nil {
return false, fmt.Errorf("failed programming expression %s: %w", expression, err)
return false, "", fmt.Errorf("failed programming expression %s: %w", expression, err)
}

// We get an error here when the value field we're looking for is not yet defined
// For now leaving it as error, in the future when we see different scenarios
// of this error we can make some a reason, and others an error
output, _, err := program.Eval(context)
if err != nil {
return false, fmt.Errorf("failed evaluating expression %s: %w", expression, err)
return false, "", fmt.Errorf("failed evaluating expression %s: %w", expression, err)
}
// We should not expect an error here as well since we checked during dry-run
out, err := celutil.ConvertCELtoGo(output)
if err != nil {
return false, fmt.Errorf("failed converting output %v: %w", output, err)
return false, "", fmt.Errorf("failed converting output %v: %w", output, err)
}
// keep checking for a false
// returning a reason here to point out which expression is not ready yet
if !out.(bool) {
return false, nil
return false, fmt.Sprintf("expression %s evaluated to false", expression), nil
}
}
return true, err
return true, "", nil
}

// containsAllElements checks if all elements in the inner slice are present
Expand Down
2 changes: 1 addition & 1 deletion internal/resourcegroup/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type Interface interface {
SetInstance(obj *unstructured.Unstructured)

// IsResourceReady returns true if the resource is ready, and false otherwise.
IsResourceReady(resourceID string) (bool, error)
IsResourceReady(resourceID string) (bool, string, error)
}

// ResourceDescriptor provides metadata about a resource.
Expand Down
11 changes: 11 additions & 0 deletions internal/typesystem/variable/variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ const (
// spec:
// vpcID: ${vpc.status.vpcID}
ResourceVariableKindDynamic ResourceVariableKind = "dynamic"
// ResourceVariableKindReadyOn represents readyOn variables. ReadyOn variables
// are resolved at runtime. The difference between them, and the dynamic variables
// is that dynamic variable resolutions wait for other resources to provide a value
// while ReadyOn variables are created and wait for certain conditions before
// moving forward to the next resource to create
//
// For example:
// name: cluster
// readyOn:
// - ${status.status == "Active"}
ResourceVariableKindReadyOn ResourceVariableKind = "readyOn"
)

// String returns the string representation of a ResourceVariableKind.
Expand Down

0 comments on commit 71a1183

Please sign in to comment.