Files
zrok/controller/automation/resource.go

199 lines
4.9 KiB
Go

package automation
import (
"context"
"fmt"
"time"
"github.com/openziti/edge-api/rest_management_api_client"
"github.com/openziti/edge-api/rest_model"
"github.com/pkg/errors"
)
type IResourceManager[T any, CreateOpts any] interface {
Create(opts CreateOpts) (string, error)
Delete(id string) error
Find(opts *FilterOptions) ([]*T, error)
GetByID(id string) (*T, error)
GetByName(name string) (*T, error)
}
type ErrorType int
const (
ErrorTypeNotFound ErrorType = iota
ErrorTypePermission
ErrorTypeValidation
ErrorTypeNetwork
ErrorTypeInternal
)
type AutomationError struct {
Type ErrorType
Resource string
Operation string
Cause error
}
func (e *AutomationError) Error() string {
return fmt.Sprintf("%s operation failed on %s: %v", e.Operation, e.Resource, e.Cause)
}
func (e *AutomationError) IsNotFound() bool {
return e.Type == ErrorTypeNotFound
}
func (e *AutomationError) IsRetryable() bool {
return e.Type == ErrorTypeNetwork
}
func NewNotFoundError(resource, operation string, cause error) *AutomationError {
return &AutomationError{
Type: ErrorTypeNotFound,
Resource: resource,
Operation: operation,
Cause: cause,
}
}
type BaseOptions struct {
Name string
Tags *Tags
TagContext map[string]interface{}
Timeout time.Duration
}
func (bo *BaseOptions) GetTimeout() time.Duration {
if bo.Timeout == 0 {
return DefaultRequestTimeout
}
return bo.Timeout
}
func (bo *BaseOptions) GetTags() *rest_model.Tags {
if bo.Tags != nil {
return bo.Tags.ToRestModel()
}
return &rest_model.Tags{SubTags: make(map[string]interface{})}
}
type BaseResourceManager[T any] struct {
client *ZitiAutomation
}
func NewBaseResourceManager[T any](ziti *ZitiAutomation) *BaseResourceManager[T] {
return &BaseResourceManager[T]{client: ziti}
}
func (brm *BaseResourceManager[T]) Edge() *rest_management_api_client.ZitiEdgeManagement {
return brm.client.Edge()
}
func (brm *BaseResourceManager[T]) Context() context.Context {
return context.Background()
}
// generic helper for GetByID operations
func GetByID[T any](finder func(*FilterOptions) ([]*T, error), id string, resourceType string) (*T, error) {
opts := &FilterOptions{Filter: BuildFilter("id", id)}
items, err := finder(opts)
if err != nil {
return nil, errors.Wrapf(err, "error finding %s by id", resourceType)
}
if len(items) == 0 {
return nil, NewNotFoundError(resourceType, "GetByID", errors.Errorf("%s with id '%s' not found", resourceType, id))
}
if len(items) != 1 {
return nil, errors.Errorf("expected 1 %s, found %d", resourceType, len(items))
}
return items[0], nil
}
// generic helper for GetByName operations
func GetByName[T any](finder func(*FilterOptions) ([]*T, error), name string, resourceType string) (*T, error) {
opts := &FilterOptions{Filter: BuildFilter("name", name)}
items, err := finder(opts)
if err != nil {
return nil, errors.Wrapf(err, "error finding %s by name", resourceType)
}
if len(items) == 0 {
return nil, NewNotFoundError(resourceType, "GetByName", errors.Errorf("%s with name '%s' not found", resourceType, name))
}
if len(items) != 1 {
return nil, errors.Errorf("expected 1 %s with name '%s', found %d", resourceType, name, len(items))
}
return items[0], nil
}
// generic helper for bulk delete operations
func DeleteWithFilter[T any](finder func(*FilterOptions) ([]*T, error), deleter func(string) error, filter string, resourceType string) error {
opts := &FilterOptions{Filter: filter}
items, err := finder(opts)
if err != nil {
return errors.Wrapf(err, "error finding %s for deletion", resourceType)
}
for _, item := range items {
id, err := getResourceID(item)
if err != nil {
return errors.Wrapf(err, "error extracting ID for %s", resourceType)
}
if err := deleter(id); err != nil {
return errors.Wrapf(err, "error deleting %s '%s'", resourceType, id)
}
}
return nil
}
// helper to extract ID from any resource type
func getResourceID(item interface{}) (string, error) {
switch v := item.(type) {
case *rest_model.IdentityDetail:
return *v.ID, nil
case *rest_model.ServiceDetail:
return *v.ID, nil
case *rest_model.ConfigDetail:
return *v.ID, nil
case *rest_model.ConfigTypeDetail:
return *v.ID, nil
case *rest_model.EdgeRouterPolicyDetail:
return *v.ID, nil
case *rest_model.ServiceEdgeRouterPolicyDetail:
return *v.ID, nil
case *rest_model.ServicePolicyDetail:
return *v.ID, nil
default:
return "", errors.Errorf("unsupported resource type: %T", item)
}
}
type FilterOptions struct {
Filter string
Limit int64
Offset int64
Timeout time.Duration
}
func (fo *FilterOptions) GetTimeout() time.Duration {
if fo.Timeout == 0 {
return DefaultRequestTimeout
}
return fo.Timeout
}
func (fo *FilterOptions) GetLimit() int64 {
if fo.Limit == 0 {
return 100
}
return fo.Limit
}
func BuildFilter(field, value string) string {
return fmt.Sprintf("%s=\"%s\"", field, value)
}
func BuildTagFilter(tag, value string) string {
return fmt.Sprintf("tags.%s=\"%s\"", tag, value)
}