package template import ( "fmt" "regexp" "strings" "k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/validation/field" "github.com/openshift/origin/pkg/template/api" . "github.com/openshift/origin/pkg/template/generator" "github.com/openshift/origin/pkg/util" "github.com/openshift/origin/pkg/util/stringreplace" ) // match ${KEY}, KEY will be grouped var stringParameterExp = regexp.MustCompile(`\$\{([a-zA-Z0-9\_]+?)\}`) // match ${{KEY}} exact match only, KEY will be grouped var nonStringParameterExp = regexp.MustCompile(`^\$\{\{([a-zA-Z0-9\_]+)\}\}$`) // Processor process the Template into the List with substituted parameters type Processor struct { Generators map[string]Generator } // NewProcessor creates new Processor and initializes its set of generators. func NewProcessor(generators map[string]Generator) *Processor { return &Processor{Generators: generators} } // Process transforms Template object into List object. It generates // Parameter values using the defined set of generators first, and then it // substitutes all Parameter expression occurrences with their corresponding // values (currently in the containers' Environment variables only). func (p *Processor) Process(template *api.Template) field.ErrorList { templateErrors := field.ErrorList{} if fieldError := p.GenerateParameterValues(template); fieldError != nil { return append(templateErrors, fieldError) } // Place parameters into a map for efficient lookup paramMap := make(map[string]api.Parameter) for _, param := range template.Parameters { paramMap[param.Name] = param } // Perform parameter substitution on the template's user message. This can be used to // instruct a user on next steps for the template. template.Message, _ = p.EvaluateParameterSubstitution(paramMap, template.Message) itemPath := field.NewPath("item") for i, item := range template.Objects { idxPath := itemPath.Index(i) if obj, ok := item.(*runtime.Unknown); ok { // TODO: use runtime.DecodeList when it returns ValidationErrorList decodedObj, err := runtime.Decode(runtime.UnstructuredJSONScheme, obj.Raw) if err != nil { templateErrors = append(templateErrors, field.Invalid(idxPath.Child("objects"), obj, fmt.Sprintf("unable to handle object: %v", err))) continue } item = decodedObj } newItem, err := p.SubstituteParameters(paramMap, item) if err != nil { templateErrors = append(templateErrors, field.Invalid(idxPath.Child("parameters"), template.Parameters, err.Error())) } // If an object definition's metadata includes a namespace field, the field will be stripped out of // the definition during template instantiation. This is necessary because all objects created during // instantiation are placed into the target namespace, so it would be invalid for the object to declare //a different namespace. stripNamespace(newItem) if err := util.AddObjectLabels(newItem, template.ObjectLabels); err != nil { templateErrors = append(templateErrors, field.Invalid(idxPath.Child("labels"), template.ObjectLabels, fmt.Sprintf("label could not be applied: %v", err))) } template.Objects[i] = newItem } return templateErrors } func stripNamespace(obj runtime.Object) { // Remove namespace from the item if itemMeta, err := meta.Accessor(obj); err == nil && len(itemMeta.GetNamespace()) > 0 { itemMeta.SetNamespace("") return } // TODO: allow meta.Accessor to handle runtime.Unstructured if unstruct, ok := obj.(*runtime.Unstructured); ok && unstruct.Object != nil { if obj, ok := unstruct.Object["metadata"]; ok { if m, ok := obj.(map[string]interface{}); ok { if _, ok := m["namespace"]; ok { m["namespace"] = "" } } return } if _, ok := unstruct.Object["namespace"]; ok { unstruct.Object["namespace"] = "" return } } } // AddParameter adds new custom parameter to the Template. It overrides // the existing parameter, if already defined. func AddParameter(t *api.Template, param api.Parameter) { if existing := GetParameterByName(t, param.Name); existing != nil { *existing = param } else { t.Parameters = append(t.Parameters, param) } } // GetParameterByName searches for a Parameter in the Template // based on its name. func GetParameterByName(t *api.Template, name string) *api.Parameter { for i, param := range t.Parameters { if param.Name == name { return &(t.Parameters[i]) } } return nil } // EvaluateParameterSubstitution replaces escaped parameters in a string with values from the // provided map. Returns the substituted value (if any substitution applied) and a boolean // indicating if the resulting value should be treated as a string(true) or a non-string // value(false) for purposes of json encoding. func (p *Processor) EvaluateParameterSubstitution(params map[string]api.Parameter, in string) (string, bool) { out := in // First check if the value matches the "${{KEY}}" substitution syntax, which // means replace and drop the quotes because the parameter value is to be used // as a non-string value. If we hit a match here, we're done because the // "${{KEY}}" syntax is exact match only, it cannot be used in a value like // "FOO_${{KEY}}_BAR", no substitution will be performed if it is used in that way. for _, match := range nonStringParameterExp.FindAllStringSubmatch(in, -1) { if len(match) > 1 { if paramValue, found := params[match[1]]; found { out = strings.Replace(out, match[0], paramValue.Value, 1) return out, false } } } // If we didn't do a non-string substitution above, do normal string substitution // on the value here if it contains a "${KEY}" reference. This substitution does // allow multiple matches and prefix/postfix, eg "FOO_${KEY1}_${KEY2}_BAR" for _, match := range stringParameterExp.FindAllStringSubmatch(in, -1) { if len(match) > 1 { if paramValue, found := params[match[1]]; found { out = strings.Replace(out, match[0], paramValue.Value, 1) } } } return out, true } // SubstituteParameters loops over all values defined in structured // and unstructured types that are children of item. // // Example of Parameter expression: // - ${PARAMETER_NAME} // func (p *Processor) SubstituteParameters(params map[string]api.Parameter, item runtime.Object) (runtime.Object, error) { stringreplace.VisitObjectStrings(item, func(in string) (string, bool) { return p.EvaluateParameterSubstitution(params, in) }) return item, nil } // GenerateParameterValues generates Value for each Parameter of the given // Template that has Generate field specified where Value is not already // supplied. // // Examples: // // from | value // ----------------------------- // "test[0-9]{1}x" | "test7x" // "[0-1]{8}" | "01001100" // "0x[A-F0-9]{4}" | "0xB3AF" // "[a-zA-Z0-9]{8}" | "hW4yQU5i" // If an error occurs, the parameter that caused the error is returned along with the error message. func (p *Processor) GenerateParameterValues(t *api.Template) *field.Error { for i := range t.Parameters { param := &t.Parameters[i] if len(param.Value) > 0 { continue } templatePath := field.NewPath("template").Child("parameters").Index(i) if param.Generate != "" { generator, ok := p.Generators[param.Generate] if !ok { err := fmt.Errorf("Unknown generator name '%v' for parameter %s", param.Generate, param.Name) return field.Invalid(templatePath, param.Generate, err.Error()) } if generator == nil { err := fmt.Errorf("template.parameters[%v]: Invalid '%v' generator for parameter %s", i, param.Generate, param.Name) return field.Invalid(templatePath, param, err.Error()) } value, err := generator.GenerateValue(param.From) if err != nil { return field.Invalid(templatePath, param, err.Error()) } param.Value, ok = value.(string) if !ok { err := fmt.Errorf("template.parameters[%v]: Unable to convert the generated value '%#v' to string for parameter %s", i, value, param.Name) return field.Invalid(templatePath, param, err.Error()) } } if len(param.Value) == 0 && param.Required { err := fmt.Errorf("template.parameters[%v]: parameter %s is required and must be specified", i, param.Name) return field.Required(templatePath, err.Error()) } } return nil }