mirror of
https://github.com/siderolabs/omni.git
synced 2025-08-09 11:06:59 +02:00
Some checks failed
default / default (push) Has been cancelled
default / e2e-backups (push) Has been cancelled
default / e2e-forced-removal (push) Has been cancelled
default / e2e-scaling (push) Has been cancelled
default / e2e-short (push) Has been cancelled
default / e2e-short-secureboot (push) Has been cancelled
default / e2e-templates (push) Has been cancelled
default / e2e-upgrades (push) Has been cancelled
default / e2e-workload-proxy (push) Has been cancelled
- The license headers in the generated test sources via `mockgen` were getting commented-out after `make generate` was run. Fix this by replacing repeated double-slashes `// //` via a single double-slash `//`. - Rekres, `make generate` and `make generate-frontend`. - Bump Go deps. - Fix linting errors to satisfy new rules in golangci-lint `v2.1.1`. Signed-off-by: Utku Ozdemir <utku.ozdemir@siderolabs.com>
307 lines
7.5 KiB
Go
307 lines
7.5 KiB
Go
// Copyright (c) 2025 Sidero Labs, Inc.
|
|
//
|
|
// Use of this software is governed by the Business Source License
|
|
// included in the LICENSE file.
|
|
|
|
// Package runtime implements connectors to various runtimes.
|
|
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
cosiresource "github.com/cosi-project/runtime/pkg/resource"
|
|
"github.com/cosi-project/runtime/pkg/resource/typed"
|
|
"github.com/siderolabs/gen/xslices"
|
|
"k8s.io/client-go/rest"
|
|
|
|
"github.com/siderolabs/omni/client/api/common"
|
|
"github.com/siderolabs/omni/client/api/omni/resources"
|
|
"github.com/siderolabs/omni/client/pkg/runtime"
|
|
)
|
|
|
|
// Runtime is an abstraction for the data access.
|
|
type Runtime interface {
|
|
Watch(context.Context, chan<- WatchResponse, ...QueryOption) error
|
|
Get(context.Context, ...QueryOption) (any, error)
|
|
List(context.Context, ...QueryOption) (ListResult, error)
|
|
Create(context.Context, cosiresource.Resource, ...QueryOption) error
|
|
Update(context.Context, cosiresource.Resource, ...QueryOption) error
|
|
Delete(context.Context, ...QueryOption) error
|
|
}
|
|
|
|
// WatchResponse is a wrapper for the resources.WatchResponse.
|
|
type WatchResponse interface {
|
|
ID() string
|
|
Namespace() string
|
|
Field(name string) (string, bool)
|
|
Match(searchFor string) bool
|
|
Unwrap() *resources.WatchResponse
|
|
}
|
|
|
|
// EventType returns event type for the response.
|
|
func EventType(resp WatchResponse) resources.EventType { return resp.Unwrap().Event.EventType }
|
|
|
|
// ListResult is a wrapper for the list result.
|
|
type ListResult struct {
|
|
Items []runtime.ListItem
|
|
Total int
|
|
}
|
|
|
|
// ListComparator is a comparator for the list items.
|
|
type ListComparator func(a, b runtime.ListItem) (int, error)
|
|
|
|
// SortInPlace sorts the list result.
|
|
func (l *ListResult) SortInPlace(cmp ListComparator) error { return unsafeSort(l.Items, cmp) }
|
|
|
|
// Slice returns a slice of the list result.
|
|
func (l *ListResult) Slice(offset, count int) ListResult {
|
|
if offset >= len(l.Items) {
|
|
return ListResult{
|
|
Total: l.Total,
|
|
}
|
|
}
|
|
|
|
items := l.Items[offset:]
|
|
|
|
if count > 0 && count < len(items) {
|
|
items = items[:count]
|
|
}
|
|
|
|
return ListResult{
|
|
Items: items,
|
|
Total: l.Total,
|
|
}
|
|
}
|
|
|
|
// Filter filters Items using the provided predicate.
|
|
func (l *ListResult) Filter(match func(m runtime.ListItem) bool) ListResult {
|
|
result := xslices.Filter(l.Items, match)
|
|
|
|
return ListResult{
|
|
Items: result,
|
|
Total: len(result),
|
|
}
|
|
}
|
|
|
|
// KubeconfigSource is implemented by runtimes that allow getting kubeconfigs.
|
|
type KubeconfigSource interface {
|
|
GetKubeconfig(context.Context, *common.Context) (*rest.Config, error)
|
|
}
|
|
|
|
var (
|
|
runtimeMu sync.RWMutex
|
|
runtimes = map[string]Runtime{}
|
|
)
|
|
|
|
// Install a runtime singleton for a type.
|
|
func Install(name string, runtime Runtime) {
|
|
runtimeMu.Lock()
|
|
defer runtimeMu.Unlock()
|
|
|
|
runtimes[name] = &proxyRuntime{runtime}
|
|
}
|
|
|
|
// Get returns runtime for a type.
|
|
func Get(name string) (Runtime, error) { //nolint:ireturn
|
|
runtimeMu.RLock()
|
|
defer runtimeMu.RUnlock()
|
|
|
|
if runtime, ok := runtimes[name]; ok {
|
|
return runtime, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to find the runtime %v", name)
|
|
}
|
|
|
|
// LookupInterface looks for a specific implementation in runtimes.
|
|
func LookupInterface[T any](name string) (T, error) {
|
|
var zero T
|
|
|
|
typ := reflect.TypeFor[T]()
|
|
if typ.Kind() != reflect.Interface {
|
|
return zero, errors.New("can only be used with interface types")
|
|
}
|
|
|
|
if typ.NumMethod() != 1 {
|
|
return zero, errors.New("can only be used with interfaces with a single method")
|
|
}
|
|
|
|
runtimeMu.RLock()
|
|
defer runtimeMu.RUnlock()
|
|
|
|
if runtime, ok := runtimes[name]; ok {
|
|
res, ok := unwrap(runtime).(T)
|
|
if !ok {
|
|
return zero, fmt.Errorf("runtime with id %s is not %s", name, typeName(typ))
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
return zero, fmt.Errorf("failed to find the runtime %v", name)
|
|
}
|
|
|
|
func typeName(typ reflect.Type) string {
|
|
if name := typ.Name(); name != "" {
|
|
return name
|
|
}
|
|
|
|
return typ.String()
|
|
}
|
|
|
|
func unwrap(runtime Runtime) Runtime {
|
|
for {
|
|
wrapped, ok := runtime.(interface{ Unwrap() Runtime })
|
|
if !ok {
|
|
return runtime
|
|
}
|
|
|
|
runtime = wrapped.Unwrap()
|
|
}
|
|
}
|
|
|
|
// NewBasicResponse creates a new basic response.
|
|
func NewBasicResponse(id string, namespace string, resp *resources.WatchResponse) BasicResponse {
|
|
mustNotNil(resp, "nil response")
|
|
mustNotNil(resp.Event, "nil response event")
|
|
|
|
return BasicResponse{
|
|
BasicItem: BasicItem[*resources.WatchResponse]{
|
|
id: id,
|
|
ns: namespace,
|
|
v: resp,
|
|
},
|
|
}
|
|
}
|
|
|
|
// BasicResponse is a basic implementation of the WatchResponse.
|
|
type BasicResponse struct {
|
|
BasicItem[*resources.WatchResponse]
|
|
}
|
|
|
|
// Field implements WatchResponse. name can be "id", "namespace" or "event_type".
|
|
// If name is empty, it returns ID. If name is unknown, it returns false.
|
|
func (b *BasicResponse) Field(name string) (string, bool) {
|
|
field, ok := b.BasicItem.Field(name)
|
|
if ok {
|
|
return field, true
|
|
}
|
|
|
|
if name == "event_type" {
|
|
return string(EventType(b)), true
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
// Match looks for a specific string in item data.
|
|
func (b *BasicResponse) Match(searchFor string) bool {
|
|
return EventType(b) == resources.EventType_BOOTSTRAPPED || b.BasicItem.Match(searchFor)
|
|
}
|
|
|
|
// String implements fmt.Stringer.
|
|
func (b *BasicResponse) String() string {
|
|
return fmt.Sprintf("{id=%q, namespace=%q, event_type=%q, total=%d}", b.id, b.ns, EventType(b), b.Unwrap().GetTotal())
|
|
}
|
|
|
|
// MakeBasicItem creates a new basic item.
|
|
func MakeBasicItem[T any](id string, ns string, v T) BasicItem[T] {
|
|
return BasicItem[T]{id: id, ns: ns, v: v}
|
|
}
|
|
|
|
// BasicItem is a basic building block for the WatchResponse and ListItem.
|
|
type BasicItem[T any] struct {
|
|
v T
|
|
id string
|
|
ns string
|
|
}
|
|
|
|
// ID implements WatchResponse.
|
|
func (bi BasicItem[T]) ID() string { return bi.id }
|
|
|
|
// Namespace implements WatchResponse.
|
|
func (bi BasicItem[T]) Namespace() string { return bi.ns }
|
|
|
|
// Unwrap implements WatchResponse.
|
|
func (bi BasicItem[T]) Unwrap() T { return bi.v }
|
|
|
|
// Field implements WatchResponse. name can be "id", "namespace" or "event_type".
|
|
// If name is empty, it returns ID. If name is unknown, it returns false.
|
|
func (bi BasicItem[T]) Field(name string) (string, bool) {
|
|
switch name {
|
|
case "id", "":
|
|
return bi.id, true
|
|
case "namespace":
|
|
return bi.ns, true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
// Match looks for a specific string in item data.
|
|
func (bi BasicItem[T]) Match(searchFor string) bool {
|
|
return strings.Contains(bi.ID(), searchFor) ||
|
|
strings.Contains(bi.Namespace(), searchFor)
|
|
}
|
|
|
|
// String implements fmt.Stringer.
|
|
func (bi BasicItem[T]) String() string {
|
|
return fmt.Sprintf("{id=%q, namespace=%q}", bi.id, bi.ns)
|
|
}
|
|
|
|
func mustNotNil[T any](v *T, msg string) {
|
|
if v == nil {
|
|
panic(msg)
|
|
}
|
|
}
|
|
|
|
// ResourceField returns a metadata field value.
|
|
func ResourceField(res cosiresource.Resource, name string) (string, bool) {
|
|
if res == nil {
|
|
return "", false
|
|
}
|
|
|
|
switch name {
|
|
case "created":
|
|
return timeToString(res.Metadata().Created()), true
|
|
case "updated":
|
|
return timeToString(res.Metadata().Updated()), true
|
|
}
|
|
|
|
fielder, ok := typed.LookupExtension[runtime.Fielder](res)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
|
|
return fielder.Field(name)
|
|
}
|
|
|
|
func timeToString(t time.Time) string {
|
|
return t.UTC().Format("2006-01-02 15:04:05.000000000")
|
|
}
|
|
|
|
// MatchResource returns true if searchFor is in any field of metadata.
|
|
func MatchResource(res cosiresource.Resource, searchFor string) bool {
|
|
if res == nil {
|
|
return false
|
|
}
|
|
|
|
if strings.Contains(timeToString(res.Metadata().Created()), searchFor) ||
|
|
strings.Contains(timeToString(res.Metadata().Updated()), searchFor) {
|
|
return true
|
|
}
|
|
|
|
matcher, ok := typed.LookupExtension[runtime.Matcher](res)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
return matcher.Match(searchFor)
|
|
}
|