Skip to content

Commit

Permalink
operator: rework label templating (#808)
Browse files Browse the repository at this point in the history
Reworked the code to provide templating to MachineInventory labels and name.
The template source data is the "System Information" (BIOS) and "System Data" (which we usually called 'HW Labels') data sent by the machines via the register client.

This rework fixes some bugs and duplicated code and slightly changes the behavior in few cases.
The behavioral changes address the following corner cases:
* `.` is added to the allowed characters in label values (previously was substituted with `-`).
* when the first character of a label value is an accepted one but not alphanumeric (i.e., `-` , `_` or `.`) we drop it (previously we prepended `m` to the label).
* if the last character of a label value is not alphanumeric (i.e., `-` , `_` or `.`) we drop it (previously was not checked).
note that for the MachineInventory name (which will also be the machine hostname after k8s provisioning) the allowed set of characters has not changed: it's the same of the label case but without the `_` (i.e., `-` or `.`).

 Regarding the MachineRegistration.spec.machineName:
* if it is empty, a default `m-${UUID}` value is assigned to the MachineInventory.name (as previously)
* if it contains a template value which doesn't exists (wrong template value or `nosmbios` option) the MachineInventory.name is assigned a default `m-${UUID}` name (new behavior).
* if it is not empty, but after resolving the template values and sanitizing the string it gets empty, the name assignment process will error out failing the registration process.

Commits:

* operator: move label templating functions to separate file
no code changes, just label templating functions moved to labeltmpl.go.

* operator: add pkg/templater

* operator: add few comments

* operator: rework label templating
Fixes #807

* tests: update api_registration_test.go

* operator: move regexp to label templating file

* operator: change template behavior in corner cases
1. '.' is added to the allowed characted in label values (previously was
   sobsituted with '-').
2. when the first character of a label value is not alphanumeric ("-" or
   "_" or ".") we drop it (previously we prepended 'm').
3. if the last characted of a label value is not alphanumeric ("-" or
   "_" or ".") we drop it (previously was not checked).

* tests: improve coverage of label templating

* make linter happy

Signed-off-by: Francesco Giudici <[email protected]>
  • Loading branch information
fgiudici authored Aug 5, 2024
1 parent 05b6a19 commit 6681ea5
Show file tree
Hide file tree
Showing 8 changed files with 607 additions and 244 deletions.
7 changes: 7 additions & 0 deletions pkg/hostinfo/hostinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ func host(opts ...*ghw.WithOption) (HostInfo, error) {

// Deprecated. Remove me together with 'MsgSystemData' type.
func FillData(data []byte) (map[string]interface{}, error) {
// Also available but not used:
// systemData.Product -> name, vendor, serial,uuid,sku,version. Kind of smbios data
// systemData.BIOS -> info about the bios. Useless IMO
// systemData.Baseboard -> asset, serial, vendor,version,product. Kind of useless?
// systemData.Chassis -> asset, serial, vendor,version,product, type. Maybe be useful depending on the provider.
// systemData.Topology -> CPU/memory and cache topology. No idea if useful.

systemData := &HostInfo{}
if err := json.Unmarshal(data, &systemData); err != nil {
return nil, fmt.Errorf("unmarshalling system data payload: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion pkg/register/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const (
MsgLabels
MsgGet // v0.5.0
MsgVersion // v1.1.0
MsgSystemData // v1.1.1
MsgSystemData // v1.1.1 deprecated by MsgSystemDataV2
MsgConfig // v1.1.1
MsgError // v1.1.1
MsgAnnotations // v1.1.4
Expand Down
244 changes: 19 additions & 225 deletions pkg/server/api_registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@ import (
"io"
"net/http"
"path"
"regexp"
"strings"

"github.com/gorilla/websocket"
values "github.com/rancher/wrangler/v2/pkg/data"
"gopkg.in/yaml.v3"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -36,21 +33,15 @@ import (
"github.com/rancher/elemental-operator/pkg/hostinfo"
"github.com/rancher/elemental-operator/pkg/log"
"github.com/rancher/elemental-operator/pkg/register"
"github.com/rancher/elemental-operator/pkg/templater"
)

type LegacyConfig struct {
Elemental elementalv1.Elemental `yaml:"elemental"`
CloudConfig map[string]interface{} `yaml:"cloud-config,omitempty"`
}

var (
sanitize = regexp.MustCompile("[^0-9a-zA-Z_]")
sanitizeHostname = regexp.MustCompile("[^0-9a-zA-Z.]")
doubleDash = regexp.MustCompile("--+")
start = regexp.MustCompile("^[a-zA-Z0-9]")
errValueNotFound = errors.New("value not found")
errInventoryNotFound = errors.New("MachineInventory not found")
)
var errInventoryNotFound = errors.New("MachineInventory not found")

func (i *InventoryServer) apiRegistration(resp http.ResponseWriter, req *http.Request) error {
var err error
Expand Down Expand Up @@ -217,36 +208,9 @@ func (i *InventoryServer) getRancherCACert() string {
return cacert
}

func replaceStringData(data map[string]interface{}, name string) (string, error) {
str := name
result := &strings.Builder{}
for {
i := strings.Index(str, "${")
if i == -1 {
result.WriteString(str)
break
}
j := strings.Index(str[i:], "}")
if j == -1 {
result.WriteString(str)
break
}

result.WriteString(str[:i])
obj := values.GetValueN(data, strings.Split(str[i+2:j+i], "/")...)
if str, ok := obj.(string); ok {
result.WriteString(str)
} else {
return "", errValueNotFound
}
str = str[j+i+1:]
}

return result.String(), nil
}

func (i *InventoryServer) serveLoop(conn *websocket.Conn, inventory *elementalv1.MachineInventory, registration *elementalv1.MachineRegistration) error {
protoVersion := register.MsgUndefined
tmpl := templater.NewTemplater()

for {
var data []byte
Expand All @@ -268,37 +232,44 @@ func (i *InventoryServer) serveLoop(conn *websocket.Conn, inventory *elementalv1
replyMsgType = register.MsgVersion
replyData = []byte{byte(protoVersion)}
case register.MsgSmbios:
err = updateInventoryFromSMBIOSData(data, inventory, registration)
if err != nil {
smbiosData := map[string]interface{}{}
if err := json.Unmarshal(data, &smbiosData); err != nil {
return fmt.Errorf("failed to extract labels from SMBIOS data: %w", err)
}
log.Debugf("received SMBIOS data - generated machine name: %s", inventory.Name)
tmpl.Fill(smbiosData)
case register.MsgLabels:
if err := mergeInventoryLabels(inventory, data); err != nil {
return err
}
case register.MsgAnnotations:
err = updateInventoryWithAnnotations(data, inventory)
err = mergeInventoryAnnotations(data, inventory)
if err != nil {
return fmt.Errorf("failed to decode dynamic data: %w", err)
}
case register.MsgGet:
// Final call: here we commit the MachineInventory, send the Elemental config data
// and close the connection.
if err := updateInventoryWithTemplates(tmpl, inventory, registration); err != nil {
return err
}
return i.handleGet(conn, protoVersion, inventory, registration)
case register.MsgUpdate:
err = i.handleUpdate(conn, protoVersion, inventory)
if err != nil {
return fmt.Errorf("failed to negotiate registration update: %w", err)
}
case register.MsgSystemData:
err = updateInventoryFromSystemData(data, inventory, registration)
systemData, err := hostinfo.FillData(data)
if err != nil {
return fmt.Errorf("failed to extract labels from system data: %w", err)
return fmt.Errorf("failed to parse system data: %w", err)
}
tmpl.Fill(systemData)
case register.MsgSystemDataV2:
err = updateInventoryFromSystemDataNG(data, inventory, registration)
if err != nil {
return fmt.Errorf("failed to extract labels from system data: %w", err)
systemData := map[string]interface{}{}
if err := json.Unmarshal(data, &systemData); err != nil {
return fmt.Errorf("failed to parse system data: %w", err)
}
tmpl.Fill(systemData)
default:
return fmt.Errorf("got unexpected message: %s", msgType)
}
Expand Down Expand Up @@ -380,180 +351,3 @@ func decodeProtocolVersion(data []byte) (register.MessageType, error) {

return protoVersion, nil
}

func updateInventoryWithAnnotations(data []byte, mInventory *elementalv1.MachineInventory) error {
annotations := map[string]string{}
if err := json.Unmarshal(data, &annotations); err != nil {
return err
}
log.Debug("Adding annotations from client data")
if mInventory.Annotations == nil {
mInventory.Annotations = map[string]string{}
}
for key, val := range annotations {
mInventory.Annotations[fmt.Sprintf("elemental.cattle.io/%s", sanitizeUserInput(key))] = sanitizeUserInput(val)
}
return nil
}

// updateInventoryFromSMBIOSData() updates mInventory Name and Labels from the MachineRegistration and the SMBIOS data
func updateInventoryFromSMBIOSData(data []byte, mInventory *elementalv1.MachineInventory, mRegistration *elementalv1.MachineRegistration) error {
smbiosData := map[string]interface{}{}
if err := json.Unmarshal(data, &smbiosData); err != nil {
return err
}
// Sanitize any lower dashes into dashes as hostnames cannot have lower dashes, and we use the inventory name
// to set the machine hostname. Also set it to lowercase
name, err := replaceStringData(smbiosData, mInventory.Name)
if err == nil {
name = sanitizeStringHostname(name)
mInventory.Name = strings.ToLower(sanitizeHostname.ReplaceAllString(name, "-"))
} else {
if errors.Is(err, errValueNotFound) {
// value not found, will be set in updateInventoryFromSystemData
log.Warningf("SMBIOS Value not found: %v", mInventory.Name)
} else {
return err
}
}

log.Debugf("Adding labels from registration")
// Add extra label info from data coming from smbios and based on the registration data
if mInventory.Labels == nil {
mInventory.Labels = map[string]string{}
}
for k, v := range mRegistration.Spec.MachineInventoryLabels {
parsedData, err := replaceStringData(smbiosData, v)
if err != nil {
if errors.Is(err, errValueNotFound) {
log.Debugf("Value not found: %v", v)
continue
}
log.Errorf("Failed parsing smbios data: %v", err.Error())
return err
}
parsedData = sanitizeString(parsedData)

log.Debugf("Parsed %s into %s with smbios data, setting it to label %s", v, parsedData, k)
mInventory.Labels[k] = strings.TrimSuffix(strings.TrimPrefix(parsedData, "-"), "-")
}
return nil
}

// updateInventoryFromSystemDataNG receives digested hardware labels from the client
func updateInventoryFromSystemDataNG(data []byte, inv *elementalv1.MachineInventory, reg *elementalv1.MachineRegistration) error {
labels := map[string]interface{}{}

if err := json.Unmarshal(data, &labels); err != nil {
return fmt.Errorf("unmarshalling system data labels payload: %w", err)
}

return sanitizeSystemDataLabels(labels, inv, reg)
}

// Deprecated. Remove me together with 'MsgSystemData' type.
// updateInventoryFromSystemData creates labels in the inventory based on the hardware information
func updateInventoryFromSystemData(data []byte, inv *elementalv1.MachineInventory, reg *elementalv1.MachineRegistration) error {
log.Infof("Adding labels from system data")

labels, err := hostinfo.FillData(data)
if err != nil {
return err
}

return sanitizeSystemDataLabels(labels, inv, reg)
}

func sanitizeSystemDataLabels(labels map[string]interface{}, inv *elementalv1.MachineInventory, reg *elementalv1.MachineRegistration) error {
// Also available but not used:
// systemData.Product -> name, vendor, serial,uuid,sku,version. Kind of smbios data
// systemData.BIOS -> info about the bios. Useless IMO
// systemData.Baseboard -> asset, serial, vendor,version,product. Kind of useless?
// systemData.Chassis -> asset, serial, vendor,version,product, type. Maybe be useful depending on the provider.
// systemData.Topology -> CPU/memory and cache topology. No idea if useful.

name, err := replaceStringData(labels, inv.Name)
if err != nil {
if errors.Is(err, errValueNotFound) {
log.Warningf("System data value not found: %v", inv.Name)
name = "m"
} else {
return err
}
}
name = sanitizeStringHostname(name)

inv.Name = strings.ToLower(sanitizeHostname.ReplaceAllString(name, "-"))

log.Debugf("Parsing labels from System Data")

if inv.Labels == nil {
inv.Labels = map[string]string{}
}

for k, v := range reg.Spec.MachineInventoryLabels {
log.Debugf("Parsing: %v : %v", k, v)

parsedData, err := replaceStringData(labels, v)
if err != nil {
if errors.Is(err, errValueNotFound) {
log.Debugf("Value not found: %v", v)
continue
}
log.Errorf("Failed parsing system data: %v", err.Error())
return err
}
parsedData = sanitizeString(parsedData)

log.Debugf("Parsed %s into %s with system data, setting it to label %s", v, parsedData, k)
inv.Labels[k] = strings.TrimSuffix(strings.TrimPrefix(parsedData, "-"), "-")
}

return nil
}

// sanitizeString will sanitize a given string by:
// replacing all invalid chars as set on the sanitize regex by dashes
// removing any double dashes resulted from the above method
// removing prefix+suffix if they are a dash
func sanitizeString(s string) string {
s1 := sanitize.ReplaceAllString(s, "-")
s2 := doubleDash.ReplaceAllString(s1, "-")
if !start.MatchString(s2) {
s2 = "m" + s2
}
if len(s2) > 58 {
s2 = s2[:58]
}
return s2
}

// like sanitizeString but allows also '.' inside "s"
func sanitizeStringHostname(s string) string {
s1 := sanitizeHostname.ReplaceAllString(s, "-")
s2 := doubleDash.ReplaceAllLiteralString(s1, "-")
if !start.MatchString(s2) {
s2 = "m" + s2
}
if len(s2) > 58 {
s2 = s2[:58]
}
return s2
}

func mergeInventoryLabels(inventory *elementalv1.MachineInventory, data []byte) error {
labels := map[string]string{}
if err := json.Unmarshal(data, &labels); err != nil {
return fmt.Errorf("cannot extract inventory labels: %w", err)
}
log.Debugf("received labels: %v", labels)
log.Warningf("received labels from registering client: no more supported, skipping")
if inventory.Labels == nil {
inventory.Labels = map[string]string{}
}
return nil
}

func isNewInventory(inventory *elementalv1.MachineInventory) bool {
return inventory.CreationTimestamp.IsZero()
}
Loading

0 comments on commit 6681ea5

Please sign in to comment.