elemental-operator/pkg/server/server.go

288 lines
8.8 KiB
Go

/*
Copyright © 2022 - 2025 SUSE LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import (
"context"
"fmt"
"html"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
managementv3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
elementalv1 "github.com/rancher/elemental-operator/api/v1beta1"
"github.com/rancher/elemental-operator/pkg/log"
"github.com/rancher/elemental-operator/pkg/network"
"github.com/rancher/elemental-operator/pkg/plainauth"
"github.com/rancher/elemental-operator/pkg/register"
"github.com/rancher/elemental-operator/pkg/tpm"
)
type authenticator interface {
Authenticate(conn *websocket.Conn, req *http.Request, registerNamespace string) (*elementalv1.MachineInventory, bool, error)
}
type InventoryServer struct {
client.Client
context.Context
authenticators []authenticator
}
func New(ctx context.Context, cl client.Client) *InventoryServer {
server := &InventoryServer{
Client: cl,
Context: ctx,
authenticators: []authenticator{
tpm.New(ctx, cl),
plainauth.New(ctx, cl),
},
}
return server
}
func (i *InventoryServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
escapedPath := sanitizeUserInput(req.URL.Path)
log.Infof("Incoming HTTP request for %s", escapedPath)
// expect /elemental/{api}...
splittedPath := strings.Split(escapedPath, "/")
if len(splittedPath) < 3 {
http.Error(resp, "", http.StatusNotFound)
return
}
// drop 'elemental'
splittedPath = splittedPath[2:]
api := splittedPath[0]
switch api {
case "registration":
if err := i.apiRegistration(resp, req); err != nil {
log.Errorf("registration: %s", err.Error())
return
}
case "seedimage":
if err := i.apiSeedImage(resp, req, splittedPath); err != nil {
log.Errorf("seedimage download: %s", err.Error())
return
}
default:
log.Errorf("Unknown API: %s", api)
http.Error(resp, fmt.Sprintf("unknwon api: %s", api), http.StatusBadRequest)
return
}
}
func sanitizeUserInput(data string) string {
escapedData := strings.Replace(data, "\n", "", -1)
escapedData = strings.Replace(escapedData, "\r", "", -1)
escapedData = html.EscapeString(escapedData)
return escapedData
}
func upgrade(resp http.ResponseWriter, req *http.Request) (*websocket.Conn, error) {
upgrader := websocket.Upgrader{
HandshakeTimeout: 5 * time.Second,
CheckOrigin: func(_ *http.Request) bool { return true },
}
conn, err := upgrader.Upgrade(resp, req, nil)
if err != nil {
return nil, err
}
_ = conn.SetWriteDeadline(time.Now().Add(register.RegistrationDeadlineSeconds * time.Second))
if err := conn.SetReadDeadline(time.Now().Add(register.RegistrationDeadlineSeconds * time.Second)); err != nil {
log.Warningf("Cannot set read deadline on the websocket: %s", err.Error())
}
return conn, err
}
func (i *InventoryServer) getValue(name string) (string, error) {
setting := &managementv3.Setting{}
if err := i.Get(i, types.NamespacedName{Name: name}, setting); err != nil {
log.Errorf("Error getting %s setting: %s", name, err.Error())
return "", err
}
return setting.Value, nil
}
func (i *InventoryServer) authMachine(conn *websocket.Conn, req *http.Request, registerNamespace string) (*elementalv1.MachineInventory, error) {
for _, auth := range i.authenticators {
machine, cont, err := auth.Authenticate(conn, req, registerNamespace)
if err != nil {
return nil, err
}
if machine != nil || !cont {
return machine, nil
}
}
return nil, nil
}
func generateInventoryName() string {
const namePrefix = "m-"
return namePrefix + uuid.NewString()
}
func initInventory(inventory *elementalv1.MachineInventory, registration *elementalv1.MachineRegistration) {
inventory.Name = registration.Spec.MachineName
if inventory.Name == "" {
inventory.Name = generateInventoryName()
}
inventory.Namespace = registration.Namespace
inventory.Annotations = registration.Spec.MachineInventoryAnnotations
// Set the labels later as we may need to do some template decoding and we need
// to get data from the client first
inventory.Labels = map[string]string{}
// Set resettable annotation on cascade from MachineRegistration spec
if registration.Spec.Config.Elemental.Reset.Enabled {
if inventory.Annotations == nil {
inventory.Annotations = map[string]string{}
}
inventory.Annotations[elementalv1.MachineInventoryResettableAnnotation] = "true"
}
// Forward network config from Registration
if registration.Spec.Config.Network.Configurator != network.ConfiguratorNone {
inventory.Spec.Network.Configurator = registration.Spec.Config.Network.Configurator
inventory.Spec.Network.Config = registration.Spec.Config.Network.Config
inventory.Spec.IPAddressPools = registration.Spec.Config.Network.IPAddresses
}
if registration.Spec.Config.Network.Configurator == "" {
inventory.Spec.Network.Configurator = network.ConfiguratorNone
}
}
func (i *InventoryServer) createMachineInventory(inventory *elementalv1.MachineInventory) (*elementalv1.MachineInventory, error) {
hashType := "TPM"
if inventory.Spec.TPMHash == "" {
if inventory.Spec.MachineHash == "" {
return nil, fmt.Errorf("machine inventory TPMHash and MachineHash are both empty")
}
hashType = "Machine"
}
mInventoryList := &elementalv1.MachineInventoryList{}
if err := i.List(i, mInventoryList); err != nil {
return nil, fmt.Errorf("cannot retrieve machine inventory list: %w", err)
}
for _, m := range mInventoryList.Items {
hash := ""
if hashType == "TPM" {
if m.Spec.TPMHash == inventory.Spec.TPMHash {
hash = m.Spec.TPMHash
}
} else {
if m.Spec.MachineHash == inventory.Spec.MachineHash {
hash = m.Spec.MachineHash
}
}
if hash != "" {
return nil, fmt.Errorf("machine inventory with %s hash '%s' already present: %s/%s",
hashType, hash, m.Namespace, m.Name)
}
}
if err := i.Create(i, inventory); err != nil {
return nil, fmt.Errorf("failed to create machine inventory %s/%s: %w", inventory.Namespace, inventory.Name, err)
}
log.Infof("new machine inventory created: %s", inventory.Name)
return inventory, nil
}
func (i *InventoryServer) updateMachineInventory(inventory *elementalv1.MachineInventory) (*elementalv1.MachineInventory, error) {
if err := i.Update(i, inventory); err != nil {
return nil, fmt.Errorf("failed to update machine inventory %s/%s: %w", inventory.Namespace, inventory.Name, err)
}
log.Infof("machine inventory updated: %s", inventory.Name)
return inventory, nil
}
func (i *InventoryServer) commitMachineInventory(inventory *elementalv1.MachineInventory) (*elementalv1.MachineInventory, error) {
var err error
if inventory.CreationTimestamp.IsZero() {
if inventory, err = i.createMachineInventory(inventory); err != nil {
return nil, fmt.Errorf("MachineInventory creation failed: %w", err)
}
} else {
if inventory, err = i.updateMachineInventory(inventory); err != nil {
return nil, fmt.Errorf("MachineInventory update failed: %w", err)
}
}
return inventory, nil
}
func (i *InventoryServer) getMachineRegistration(token string) (*elementalv1.MachineRegistration, error) {
escapedToken := strings.Replace(token, "\n", "", -1)
escapedToken = strings.Replace(escapedToken, "\r", "", -1)
mRegistrationList := &elementalv1.MachineRegistrationList{}
if err := i.List(i, mRegistrationList); err != nil {
return nil, fmt.Errorf("failed to list machine registrations")
}
var mRegistration *elementalv1.MachineRegistration
// TODO: build machine registrations cache indexed by token
for _, m := range mRegistrationList.Items {
if m.Status.RegistrationToken == escapedToken {
// Found two registrations with the same registration token
if mRegistration != nil {
return nil, fmt.Errorf("machine registrations %s/%s and %s/%s have the same registration token %s",
mRegistration.Namespace, mRegistration.Name, m.Namespace, m.Name, escapedToken)
}
mRegistration = (&m).DeepCopy()
}
}
if mRegistration == nil {
return nil, fmt.Errorf("failed to find machine registration with registration token %s", escapedToken)
}
var ready bool
for _, condition := range mRegistration.Status.Conditions {
if condition.Type == "Ready" && condition.Status == "True" {
ready = true
break
}
}
if !ready {
return nil, fmt.Errorf("MachineRegistration %s/%s is not ready", mRegistration.Namespace, mRegistration.Name)
}
return mRegistration, nil
}