Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validation logic for references to outputs from prior modules #840

Merged
merged 4 commits into from
Jan 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,17 @@ var errorMessages = map[string]string{
"yamlMarshalError": "failed to marshal the yaml config",
"fileSaveError": "failed to write the expanded yaml",
// expand
"missingSetting": "a required setting is missing from a module",
"globalLabelType": "deployment variable 'labels' are not a map",
"settingsLabelType": "labels in module settings are not a map",
"invalidVar": "invalid variable definition in",
"varNotFound": "Could not find source of variable",
"varInAnotherGroup": "References to other groups are not yet supported",
"noOutput": "Output not found for a variable",
"missingSetting": "a required setting is missing from a module",
"globalLabelType": "deployment variable 'labels' are not a map",
"settingsLabelType": "labels in module settings are not a map",
"invalidVar": "invalid variable definition in",
"invalidDeploymentRef": "invalid deployment-wide reference (only \"vars\") is supported)",
"varNotFound": "Could not find source of variable",
"varInAnotherGroup": "References to other groups are not yet supported",
"intergroupImplicit": "References to outputs from other groups must explicitly identify the group",
"intergroupOrder": "References to outputs from other groups must be to earlier groups",
"referenceWrongGroup": "Reference specified the wrong group for the module",
"noOutput": "Output not found for a variable",
// validator
"emptyID": "a module id cannot be empty",
"emptySource": "a module source cannot be empty",
Expand Down
157 changes: 103 additions & 54 deletions pkg/config/expand.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,25 +420,25 @@ A variable reference has the following fields
should be set to "deployment" to indicate that the reference belongs
to the entire blueprint, rather than a deployment group.
- Name: the name of the module output or deployment variable
- Explicit: a boolean value indicating whether the user made an explicit
reference to GroupID or whether it was automatically assigned the GroupID
from which the reference was made.
- ExplicitInterGroup: a boolean value indicating whether the user made a
reference that superficially appears to be intergroup (i.e. they used
an explicit GroupID that differs from the context's GroupID)
*/
type varReference struct {
ID string
GroupID string
Name string
Explicit bool
ID string
GroupID string
Name string
ExplicitInterGroup bool
}

/*
This function performs only the most rudimentary conversion of an input
string into a varReference struct as defined above. An input string consists of
2 or 3 fields separated by periods. An error will be returned if there are not
2 or 3 fields, or if any field other than GroupID is the empty string. This
function does not ensure the existence of the reference!
2 or 3 fields, or if any field is the empty string. This function does not
ensure the existence of the reference!
*/
func identifySimpleVariable(yamlReference string, callingGroupID string) (varReference, error) {
func (dg *DeploymentGroup) identifySimpleVariable(yamlReference string) (varReference, error) {
varComponents := strings.Split(yamlReference, ".")

// struct defaults: empty strings and false booleans
Expand All @@ -451,74 +451,83 @@ func identifySimpleVariable(yamlReference string, callingGroupID string) (varRef

if ref.ID == "vars" {
ref.GroupID = "deployment"
ref.Explicit = true
} else {
ref.GroupID = callingGroupID
ref.GroupID = dg.Name
}
case 3:
ref.GroupID = varComponents[0]
ref.ID = varComponents[1]
ref.Name = varComponents[2]
ref.Explicit = true
ref.ExplicitInterGroup = ref.GroupID != dg.Name
}

// should consider more sophisticated definition of valid values here.
// for now check that source and name are not empty strings; due to the
// default zero values for strings in the "ref" struct, this will also
// cover the case that varComponents has wrong # of fields
if ref.ID == "" || ref.Name == "" {
if ref.GroupID == "" || ref.ID == "" || ref.Name == "" {
return varReference{}, fmt.Errorf("%s %s, expected format: %s",
errorMessages["invalidVar"], yamlReference, expectedVarFormat)
}
return ref, nil
}

// Needs DeploymentGroups, variable string, current group,
func expandSimpleVariable(
context varContext,
modToGrp map[string]int) (string, error) {

// Get variable contents
re := regexp.MustCompile(simpleVariableExp)
contents := re.FindStringSubmatch(context.varString)
if len(contents) != 2 { // Should always be (match, contents) here
err := fmt.Errorf("%s %s, failed to extract contents: %v",
errorMessages["invalidVar"], context.varString, contents)
return "", err
}

callingGroupID := context.blueprint.DeploymentGroups[context.groupIndex].Name

// Break up variable into source and value
ref, err := identifySimpleVariable(contents[1], callingGroupID)
if err != nil {
return "", err
}

if ref.GroupID == "deployment" && ref.ID == "vars" { // Global variable
// Verify global variable exists
if _, ok := context.blueprint.Vars[ref.Name]; !ok {
return "", fmt.Errorf("%s: %s is not a deployment variable",
errorMessages["varNotFound"], context.varString)
// this function validates every field within a varReference struct and that
// the reference must be to the same or earlier group.
// ref.GroupID: this group must exist or be the value "deployment"
// ref.ID: must be an existing module ID or "vars" (if groupID is "deployment")
// ref.Name: must match a module output name or deployment variable name
// ref.ExplicitInterGroup: intergroup references must explicitly identify the
// target group ID and intragroup references cannot have an incorrect explicit
// group ID
func validateReference(ref varReference, context varContext, modToGrp map[string]int) error {
// simplest case to evaluate is a deployment variable's existence
if ref.GroupID == "deployment" {
if ref.ID == "vars" {
if _, ok := context.blueprint.Vars[ref.Name]; !ok {
return fmt.Errorf("%s: %s is not a deployment variable",
errorMessages["varNotFound"], ref.Name)
}
return nil
}
return fmt.Sprintf("((var.%s))", ref.Name), nil
return fmt.Errorf("%s: %s", errorMessages["invalidDeploymentRef"], ref.ID)
}

// Module variable
// Verify module exists
// at this point, the reference is to a module output, not a deployment
// variable. find the deployment group in which target module exists
refGrpIndex, ok := modToGrp[ref.ID]
if !ok {
return "", fmt.Errorf("%s: module %s was not found",
return fmt.Errorf("%s: module %s was not found",
errorMessages["varNotFound"], ref.ID)
}
refGrp := context.blueprint.DeploymentGroups[refGrpIndex]

if refGrpIndex != context.groupIndex {
return "", fmt.Errorf("%s: module %s was defined in group %d and called from group %d",
errorMessages["varInAnotherGroup"], ref.ID, refGrpIndex, context.groupIndex)
// at this point, we know the target module exists. now record whether it
// is intergroup and whether it comes in a (disallowed) later group
isInterGroupReference := refGrpIndex != context.groupIndex
isLaterGroup := refGrpIndex > context.groupIndex

// intergroup references must be explicit about group and refer to an earlier group;
if isInterGroupReference {
if isLaterGroup {
return fmt.Errorf("%s: %s is in a later group",
errorMessages["intergroupOrder"], context.varString)
}

if !ref.ExplicitInterGroup {
return fmt.Errorf("%s: %s must specify a group ID before the module ID",
errorMessages["intergroupImplicit"], context.varString)
}
} else if ref.ExplicitInterGroup {
// intragroup references may be explicit or implicit; if explicit must
// be correct
return fmt.Errorf("%s: %s",
errorMessages["referenceWrongGroup"], context.varString)
}

// Get the module info
refGrp := context.blueprint.DeploymentGroups[refGrpIndex]
// at this point, we have a valid intragroup or intergroup references to a
// module. must now determine whether the output value actually exists in
// the module.
refModIndex := slices.IndexFunc(refGrp.Modules, func(m Module) bool { return m.ID == ref.ID })
if refModIndex == -1 {
log.Fatalf("Could not find module referenced by variable %s",
Expand All @@ -531,14 +540,54 @@ func expandSimpleVariable(
"failed to get info for module at %s while expanding variables: %e",
refMod.Source, err)
}

// Verify output exists in module
found := slices.ContainsFunc(modInfo.Outputs, func(o modulereader.VarInfo) bool { return o.Name == ref.Name })
if !found {
return "", fmt.Errorf("%s: module %s did not have output %s",
return fmt.Errorf("%s: module %s did not have output %s",
errorMessages["noOutput"], refMod.ID, ref.Name)
}
return fmt.Sprintf("((module.%s.%s))", ref.ID, ref.Name), nil

return nil
}

// Needs DeploymentGroups, variable string, current group,
func expandSimpleVariable(
context varContext,
modToGrp map[string]int) (string, error) {

// Get variable contents
re := regexp.MustCompile(simpleVariableExp)
contents := re.FindStringSubmatch(context.varString)
if len(contents) != 2 { // Should always be (match, contents) here
err := fmt.Errorf("%s %s, failed to extract contents: %v",
errorMessages["invalidVar"], context.varString, contents)
return "", err
}

callingGroup := context.blueprint.DeploymentGroups[context.groupIndex]
refStr := contents[1]

ref, err := callingGroup.identifySimpleVariable(refStr)
if err != nil {
return "", err
}

err = validateReference(ref, context, modToGrp)
if err != nil {
return "", err
}

var expandedVariable string
if ref.GroupID == "deployment" {
expandedVariable = fmt.Sprintf("((var.%s))", ref.Name)
} else {
if ref.ExplicitInterGroup {
expandedVariable = fmt.Sprintf("((var.%s_%s))", ref.Name, ref.ID)
return "", fmt.Errorf("%s: %s is an intergroup reference",
errorMessages["varInAnotherGroup"], context.varString)
}
expandedVariable = fmt.Sprintf("((module.%s.%s))", ref.ID, ref.Name)
}
return expandedVariable, nil
}

func expandVariable(
Expand Down
Loading