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

operator: rework label templating #808

Merged
merged 9 commits into from
Aug 5, 2024
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