From 433c35fb94536dd0ee748b11b42001359cadaf82 Mon Sep 17 00:00:00 2001 From: Rangel Ivanov Date: Mon, 25 Mar 2024 13:24:13 +0200 Subject: [PATCH] Major refactoring & implement standalone mode (#12) * Major refactoring & start work on standalone mode (part 1) Overhaul the code from a PoC to a semi-productive state. Major changes done to the Processors, mainly the UpdateWatchConfig handler, which provisions k8s managers. Simplify large parts of the codebase. Handle context cancellation propagation properly. Ensure resource cleanup on graceful shutdown - do not leave hanging goroutines. * Start gRPC stream with proper context * Refactoring part 2 Fix comments Exit Pod with status 1 in case the Manager can't be started (in K8s mode) Remove caching of auth header in HTTP executor Improvement of error messages * Fix client cert creation in standalone mode * Update env vars names * Improve logging and fix some issues * Handle session auto-config better * Delete redundant cache struct * Add some more logs to task processors * Fix k8s managers and controllers * Renaming * Fix some TODOs * Fix build * Refactoring final: Reimplement auth header caching properly * Small cleanup * Bump vulnerable dependencies * Fix basic auth header * Fix comments --- cmd/remote-work-processor/cmd_options.go | 47 +++ cmd/remote-work-processor/main.go | 169 +++++++---- go.mod | 22 +- go.sum | 44 ++- internal/cache/cache.go | 14 - internal/cache/errors.go | 17 -- internal/cache/in-memory-cache.go | 63 ---- internal/executors/enum.go | 8 - internal/executors/errors.go | 61 +--- internal/executors/executor.go | 2 +- internal/executors/executor_context.go | 20 +- internal/executors/executor_result.go | 14 +- internal/executors/executor_status.go | 22 -- internal/executors/executor_type.go | 22 -- .../executors/factory/executor_factory.go | 58 +--- .../executors/factory/executor_generator.go | 21 -- .../executors/http/authorization_header.go | 169 ++--------- .../http/basic_authorization_header.go | 9 +- internal/executors/http/csrf_token_fetcher.go | 63 ++-- internal/executors/http/errors.go | 24 +- .../http/external_authorization_header.go | 15 - internal/executors/http/generator.go | 7 +- internal/executors/http/grant_type.go | 4 - internal/executors/http/http_client.go | 10 +- internal/executors/http/http_executor.go | 221 ++++---------- .../http/http_executor_parameters.go | 274 ++++++++---------- internal/executors/http/http_response.go | 78 ++--- .../http/ias_authorization_header.go | 15 +- internal/executors/http/ias_token_fetcher.go | 25 +- internal/executors/http/oauth_header.go | 124 -------- .../executors/http/oauth_header_generator.go | 221 ++++++-------- .../http/oauth_header_generator_factory.go | 151 ++++++++++ .../http/{token.go => oauth_token.go} | 16 +- .../executors/http/oauth_token_fetcher.go | 28 +- .../http/tls/tls_configuration_provider.go | 64 ++-- internal/executors/http/token_fetcher.go | 2 +- internal/executors/http/token_type.go | 4 - internal/executors/void/void_executor.go | 12 +- internal/functional/types.go | 13 - internal/grpc/client.go | 172 ++++++----- internal/grpc/client_metadata.go | 87 ++++-- internal/grpc/processors/disable_processor.go | 25 +- internal/grpc/processors/enable_processor.go | 25 +- internal/grpc/processors/errors.go | 21 -- .../processors/probe_session_processor.go | 41 --- internal/grpc/processors/processor.go | 39 +-- internal/grpc/processors/processor_factory.go | 59 ++-- .../grpc/processors/remote_task_processor.go | 72 ++--- .../update_configuration_processor.go | 88 +++--- internal/kubernetes/controller/controller.go | 100 ------- .../controller/controller_builder.go | 91 ++++++ .../kubernetes/controller/manager-builder.go | 110 ------- .../kubernetes/controller/manager-engine.go | 69 ----- internal/kubernetes/controller/manager.go | 46 ++- .../kubernetes/controller/manager_builder.go | 72 +++++ .../kubernetes/controller/manager_engine.go | 70 +++++ internal/kubernetes/controller/reconciler.go | 88 +++--- .../reconciliation_event_provider.go | 23 +- internal/kubernetes/dynamic/dynamic_client.go | 43 +-- internal/kubernetes/engine/engine.go | 10 +- internal/kubernetes/metadata/metadata.go | 80 ++--- .../kubernetes/selector/field-selector.go | 23 +- .../kubernetes/selector/label-selector.go | 4 +- internal/kubernetes/selector/selector.go | 4 +- internal/utils/array/utils.go | 33 --- internal/utils/array_utils.go | 10 + internal/utils/env_utils.go | 15 + .../utils/{json/utils.go => json_utils.go} | 2 +- internal/utils/maps/utils.go | 39 --- internal/utils/set/type.go | 50 ---- internal/utils/tuple/pair.go | 13 - 71 files changed, 1550 insertions(+), 2227 deletions(-) create mode 100644 cmd/remote-work-processor/cmd_options.go delete mode 100644 internal/cache/cache.go delete mode 100644 internal/cache/errors.go delete mode 100644 internal/cache/in-memory-cache.go delete mode 100644 internal/executors/enum.go delete mode 100644 internal/executors/executor_status.go delete mode 100644 internal/executors/executor_type.go delete mode 100644 internal/executors/factory/executor_generator.go delete mode 100644 internal/executors/http/external_authorization_header.go delete mode 100644 internal/executors/http/oauth_header.go create mode 100644 internal/executors/http/oauth_header_generator_factory.go rename internal/executors/http/{token.go => oauth_token.go} (58%) delete mode 100644 internal/functional/types.go delete mode 100644 internal/grpc/processors/errors.go delete mode 100644 internal/grpc/processors/probe_session_processor.go delete mode 100644 internal/kubernetes/controller/controller.go create mode 100644 internal/kubernetes/controller/controller_builder.go delete mode 100644 internal/kubernetes/controller/manager-builder.go delete mode 100644 internal/kubernetes/controller/manager-engine.go create mode 100644 internal/kubernetes/controller/manager_builder.go create mode 100644 internal/kubernetes/controller/manager_engine.go delete mode 100644 internal/utils/array/utils.go create mode 100644 internal/utils/array_utils.go create mode 100644 internal/utils/env_utils.go rename internal/utils/{json/utils.go => json_utils.go} (96%) delete mode 100644 internal/utils/maps/utils.go delete mode 100644 internal/utils/set/type.go delete mode 100644 internal/utils/tuple/pair.go diff --git a/cmd/remote-work-processor/cmd_options.go b/cmd/remote-work-processor/cmd_options.go new file mode 100644 index 0000000..5b6eb9d --- /dev/null +++ b/cmd/remote-work-processor/cmd_options.go @@ -0,0 +1,47 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "flag" + "io" + "log" + "os" +) + +type Options struct { + DisplayVersion bool + StandaloneMode bool + InstanceId string + MaxConnRetries uint +} + +const ( + standaloneModeOpt = "standalone-mode" + instanceIdOpt = "instance-id" + connRetriesOpt = "conn-retries" + versionOpt = "version" +) + +func (opts *Options) BindFlags(fs *flag.FlagSet) { + hostname := getHashedHostname() + + fs.BoolVar(&opts.StandaloneMode, standaloneModeOpt, false, + "Whether to run the Remote Work Processor in Standalone mode") + fs.StringVar(&opts.InstanceId, instanceIdOpt, hostname, + "Instance Identifier for the Remote Work Processor (only applicable for Standalone mode)") + fs.UintVar(&opts.MaxConnRetries, connRetriesOpt, 3, "Number of retries for gRPC connection to AutoPi server") + fs.BoolVar(&opts.DisplayVersion, versionOpt, false, "Display binary version and exit") +} + +func getHashedHostname() string { + hostname, err := os.Hostname() + if err != nil { + log.Printf("could not get hostname: %v\n", err) + } else { + hasher := sha256.New() + io.WriteString(hasher, hostname) + hostname = hex.EncodeToString(hasher.Sum(nil)) + } + return hostname +} diff --git a/cmd/remote-work-processor/main.go b/cmd/remote-work-processor/main.go index 07143d1..6deff76 100644 --- a/cmd/remote-work-processor/main.go +++ b/cmd/remote-work-processor/main.go @@ -17,80 +17,147 @@ limitations under the License. package main import ( - // "flag" - // "os" - - // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) - // to ensure that exec-entrypoint and run can make use of them. - + "context" + "flag" + "fmt" + "github.com/SAP/remote-work-processor/internal/grpc" + "github.com/SAP/remote-work-processor/internal/grpc/processors" + "github.com/SAP/remote-work-processor/internal/kubernetes/controller" + meta "github.com/SAP/remote-work-processor/internal/kubernetes/metadata" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" "log" "os" - + "os/signal" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "syscall" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - - // "sigs.k8s.io/controller-runtime/pkg/healthz" - // "sigs.k8s.io/controller-runtime/pkg/log/zap" - // "github.com/SAP/remote-work-processor/kubernetes/controllers" - "github.com/SAP/remote-work-processor/internal/grpc" - "github.com/SAP/remote-work-processor/internal/grpc/processors" - "github.com/SAP/remote-work-processor/internal/kubernetes/controller" - "github.com/SAP/remote-work-processor/internal/kubernetes/metadata" //+kubebuilder:scaffold:imports ) var ( - scheme = runtime.NewScheme() + // Version of the Remote Work Processor. + // Injected at linking time via ldflags. + Version string + // BuildDate of the Remote Work Processor. + // Injected at linking time via ldflags. + BuildDate string ) -func init() { - utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - //+kubebuilder:scaffold:scheme -} - func main() { - metadata.InitRemoteWorkProcessorMetadata() - config := getKubeConfig() + opts := setupFlagsAndLogger() - e := controller.CreateManagerEngine(scheme, config) - processors.InitProcessorFactory(e) - grpc.InitRemoteWorkProcessorGrpcClient() + if opts.DisplayVersion { + fmt.Printf("rwp-%s Built: %s\n", Version, BuildDate) + return + } - opc := grpc.Client.Receive() + rootCtx, _ := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) - for { - op := <-opc - p, err := processors.Factory.CreateProcessor(op) - if err != nil { - log.Fatalf("Error occurred while creating operation processor: %v\n", err) - } + rwpMetadata := meta.LoadMetadata(opts.InstanceId, Version) + grpcClient := grpc.NewClient(rwpMetadata, opts.StandaloneMode) + var drainChan chan struct{} - res := <-p.Process() - if res.Err != nil { - log.Fatalf("Error occurred while processing operation: %v\n", err) - } + var factory processors.ProcessorFactory + + if opts.StandaloneMode { + factory = processors.NewStandaloneProcessorFactory() + } else { + config := getKubeConfig() + scheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme - if res.Result != nil { - grpc.Client.Send(res.Result) + drainChan = make(chan struct{}, 1) + engine := controller.CreateManagerEngine(scheme, config, grpcClient) + factory = processors.NewKubernetesProcessorFactory(engine, drainChan) + } + + connAttemptChan := make(chan struct{}, 1) + connAttemptChan <- struct{}{} + var connAttempts uint = 0 + +Loop: + for connAttempts < opts.MaxConnRetries { + select { + case <-rootCtx.Done(): + log.Println("Received cancellation signal. Stopping Remote Work Processor...") + break Loop + case <-connAttemptChan: + err := grpcClient.InitSession(rootCtx, rwpMetadata.SessionID()) + if err != nil { + signalRetry(&connAttempts, connAttemptChan, err) + } + default: + operation, err := grpcClient.ReceiveMsg() + if err != nil { + signalRetry(&connAttempts, connAttemptChan, err) + continue + } + if operation == nil { + // this flow is when the gRPC connection is closed (either by the server or the context has been cancelled) + connAttemptChan <- struct{}{} + // do not increment the retries, as this isn't a failure + continue + } + + log.Printf("Creating processor for operation: %T\n", operation.Body) + processor, err := factory.CreateProcessor(operation) + if err != nil { + log.Printf("error creating operation processor: %v\n", err) + continue + } + + msg, err := processor.Process(rootCtx) + if err != nil { + signalRetry(&connAttempts, connAttemptChan, fmt.Errorf("error processing operation: %v", err)) + continue + } + if msg == nil { + continue + } + + if err = grpcClient.Send(msg); err != nil { + signalRetry(&connAttempts, connAttemptChan, err) + } } } + + if !opts.StandaloneMode { + // wait for context cancellation to be propagated to the k8s manager + <-drainChan + } } -func getKubeConfig() *rest.Config { - rules := clientcmd.NewDefaultClientConfigLoadingRules() - overrides := &clientcmd.ConfigOverrides{} +func setupFlagsAndLogger() *Options { + opts := &Options{} + opts.BindFlags(flag.CommandLine) - kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides) + zapOpts := zap.Options{} + zapOpts.BindFlags(flag.CommandLine) - config, err := kubeConfig.ClientConfig() + flag.Parse() + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&zapOpts))) + return opts +} + +func getKubeConfig() *rest.Config { + config, err := rest.InClusterConfig() if err != nil { - os.Exit(1) + log.Fatalln("Could not create kubeconfig:", err) } - return config } + +func signalRetry(attempts *uint, retryChan chan<- struct{}, err error) { + if err != nil { + log.Println(err) + } + retryChan <- struct{}{} + *attempts++ +} diff --git a/go.mod b/go.mod index 1faca53..c4dbadc 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,8 @@ go 1.20 require ( github.com/itchyny/gojq v0.12.12 - github.com/pkg/errors v0.9.1 - google.golang.org/grpc v1.55.0 - google.golang.org/protobuf v1.30.0 + google.golang.org/grpc v1.58.3 + google.golang.org/protobuf v1.31.0 k8s.io/apimachinery v0.26.1 k8s.io/client-go v0.26.1 sigs.k8s.io/controller-runtime v0.14.6 @@ -20,6 +19,7 @@ require ( github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/zapr v1.2.3 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/swag v0.19.14 // indirect @@ -39,20 +39,24 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/oauth2 v0.6.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/term v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/oauth2 v0.10.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 624d200..0802a9f 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -81,9 +83,11 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= +github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -260,6 +264,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -271,9 +276,14 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -342,8 +352,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -351,8 +361,8 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= +golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -403,12 +413,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -416,8 +426,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -438,6 +448,7 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -525,8 +536,8 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -539,8 +550,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -553,8 +564,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -574,6 +585,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/cache/cache.go b/internal/cache/cache.go deleted file mode 100644 index 9ca3bcb..0000000 --- a/internal/cache/cache.go +++ /dev/null @@ -1,14 +0,0 @@ -package cache - -type Cache[K comparable, V any] interface { - Read(k K) V - Write(k K, v V) V - Remove(k K) - Size() int -} - -type MapCache[K comparable, V any] interface { - Cache[K, V] - FromMap(m map[K]V) MapCache[K, V] - ToMap() map[K]V -} diff --git a/internal/cache/errors.go b/internal/cache/errors.go deleted file mode 100644 index 867cfbe..0000000 --- a/internal/cache/errors.go +++ /dev/null @@ -1,17 +0,0 @@ -package cache - -import "fmt" - -type NoSuchElementError[K any] struct { - key any -} - -func NewNoSuchElementError[K any](key K) *NoSuchElementError[K] { - return &NoSuchElementError[K]{ - key: key, - } -} - -func (e *NoSuchElementError[K]) Error() string { - return fmt.Sprintf("Value mapped to key '%v' does not exist in cache", e.key) -} diff --git a/internal/cache/in-memory-cache.go b/internal/cache/in-memory-cache.go deleted file mode 100644 index 5c89d5f..0000000 --- a/internal/cache/in-memory-cache.go +++ /dev/null @@ -1,63 +0,0 @@ -package cache - -import ( - "sync" -) - -type InMemoryCache[K comparable, V any] struct { - sync.RWMutex - entries map[K]V -} - -func NewInMemoryCache[K comparable, V any]() *InMemoryCache[K, V] { - return &InMemoryCache[K, V]{ - entries: make(map[K]V), - } -} - -func (c *InMemoryCache[K, V]) FromMap(m map[K]V) MapCache[K, V] { - if m == nil { - c.entries = map[K]V{} - } else { - for k, v := range m { - c.entries[k] = v - } - } - - return c -} - -func (c *InMemoryCache[K, V]) ToMap() map[K]V { - return c.entries -} - -func (c *InMemoryCache[K, V]) Read(k K) V { - c.RLock() - defer c.RUnlock() - - v, ok := c.entries[k] - if !ok { - return *new(V) - } - - return v -} - -func (c *InMemoryCache[K, V]) Write(k K, v V) V { - c.Lock() - defer c.Unlock() - - c.entries[k] = v - return v -} - -func (c *InMemoryCache[K, V]) Remove(k K) { - c.Lock() - defer c.Unlock() - - delete(c.entries, k) -} - -func (c *InMemoryCache[K, V]) Size() int { - return len(c.entries) -} diff --git a/internal/executors/enum.go b/internal/executors/enum.go deleted file mode 100644 index d1f3aff..0000000 --- a/internal/executors/enum.go +++ /dev/null @@ -1,8 +0,0 @@ -package executors - -import "fmt" - -type Enumer interface { - fmt.Stringer - Ordinal() uint -} diff --git a/internal/executors/errors.go b/internal/executors/errors.go index 42647b1..b0ef681 100644 --- a/internal/executors/errors.go +++ b/internal/executors/errors.go @@ -1,42 +1,15 @@ package executors -import ( - "fmt" - "log" +import "fmt" - pb "github.com/SAP/remote-work-processor/build/proto/generated" -) +type RequiredKeyValidationError string -type RequiredKeyValidationError struct { - key string +func NewRequiredKeyValidationError(key string) error { + return RequiredKeyValidationError(key) } -func NewRequiredKeyValidationError(key string) *RequiredKeyValidationError { - if len(key) == 0 { - log.Fatal("Key cannot be blank") - } - - return &RequiredKeyValidationError{ - key: key, - } -} - -func (err *RequiredKeyValidationError) Error() string { - return fmt.Sprintf("Key '%s' is required but it had not been provided", err.key) -} - -type ExecutorCreationError struct { - t pb.TaskType -} - -func NewExecutorCreationError(t pb.TaskType) *ExecutorCreationError { - return &ExecutorCreationError{ - t: t, - } -} - -func (err *ExecutorCreationError) Error() string { - return fmt.Sprintf("Cannot create executor of type '%s'", err.t) +func (err RequiredKeyValidationError) Error() string { + return fmt.Sprintf("key %q is required but not provided", string(err)) } type NonRetryableError struct { @@ -44,9 +17,9 @@ type NonRetryableError struct { cause error } -func NewNonRetryableError(msg string) *NonRetryableError { +func NewNonRetryableError(format string, args ...any) *NonRetryableError { return &NonRetryableError{ - msg: msg, + msg: fmt.Sprintf(format, args...), } } @@ -68,9 +41,9 @@ type RetryableError struct { err error } -func NewRetryableError(msg string) *RetryableError { +func NewRetryableError(format string, args ...any) *RetryableError { return &RetryableError{ - msg: msg, + msg: fmt.Sprintf(format, args...), } } @@ -86,17 +59,3 @@ func (err *RetryableError) Error() string { func (err *RetryableError) Unwrap() error { return err.err } - -type InvalidHttpMethodError struct { - m string -} - -func NewInvalidHttpMethodError(m string) *InvalidHttpMethodError { - return &InvalidHttpMethodError{ - m: m, - } -} - -func (err *InvalidHttpMethodError) Error() string { - return fmt.Sprintf("'%s' is not a valid HTTP method.", err.m) -} diff --git a/internal/executors/executor.go b/internal/executors/executor.go index 9225c36..0417f73 100644 --- a/internal/executors/executor.go +++ b/internal/executors/executor.go @@ -1,5 +1,5 @@ package executors type Executor interface { - Execute(ctx ExecutorContext) *ExecutorResult + Execute(Context) *ExecutorResult } diff --git a/internal/executors/executor_context.go b/internal/executors/executor_context.go index 98ceaba..dcbb43b 100644 --- a/internal/executors/executor_context.go +++ b/internal/executors/executor_context.go @@ -3,10 +3,7 @@ package executors import ( "encoding/json" "errors" - "fmt" "strconv" - - "github.com/SAP/remote-work-processor/internal/cache" ) type Context interface { @@ -17,12 +14,12 @@ type Context interface { GetMap(k string) (map[string]string, error) GetList(k string) ([]string, error) GetBoolean(k string) (bool, error) - GetStore() cache.MapCache[string, string] + GetStore() map[string]string } type ExecutorContext struct { input map[string]string - store cache.MapCache[string, string] + store map[string]string } var ( @@ -32,10 +29,13 @@ var ( } ) -func NewExecutorContext(input map[string]string, store map[string]string) ExecutorContext { - return ExecutorContext{ +func NewExecutorContext(input map[string]string, store map[string]string) Context { + if store == nil { + store = make(map[string]string) + } + return &ExecutorContext{ input: input, - store: cache.NewInMemoryCache[string, string]().FromMap(store), + store: store, } } @@ -109,12 +109,12 @@ func (e *ExecutorContext) GetBoolean(k string) (bool, error) { b, ok := bools[s] if !ok { - return false, NewNonRetryableError(fmt.Sprintf("Input value '%s' for key '%s' is not a valid boolean", s, k)) + return false, NewNonRetryableError("Input value %q for key %q is not a valid boolean", s, k) } return b, nil } -func (e *ExecutorContext) GetStore() cache.MapCache[string, string] { +func (e *ExecutorContext) GetStore() map[string]string { return e.store } diff --git a/internal/executors/executor_result.go b/internal/executors/executor_result.go index 7f812d5..1f72d97 100644 --- a/internal/executors/executor_result.go +++ b/internal/executors/executor_result.go @@ -3,14 +3,14 @@ package executors import pb "github.com/SAP/remote-work-processor/build/proto/generated" type ExecutorResult struct { - Output map[string]any + Output map[string]string Status pb.TaskExecutionResponseMessage_TaskState Error string } -type executorResultOption func(*ExecutorResult) +type ExecutorResultOption func(*ExecutorResult) -func NewExecutorResult(opts ...executorResultOption) *ExecutorResult { +func NewExecutorResult(opts ...ExecutorResultOption) *ExecutorResult { r := &ExecutorResult{} for _, opt := range opts { @@ -20,19 +20,19 @@ func NewExecutorResult(opts ...executorResultOption) *ExecutorResult { return r } -func Output(m map[string]any) executorResultOption { +func Output(m map[string]string) ExecutorResultOption { return func(er *ExecutorResult) { er.Output = m } } -func Status(s pb.TaskExecutionResponseMessage_TaskState) executorResultOption { +func Status(s pb.TaskExecutionResponseMessage_TaskState) ExecutorResultOption { return func(er *ExecutorResult) { er.Status = s } } -func Error(err error) executorResultOption { +func Error(err error) ExecutorResultOption { return func(er *ExecutorResult) { if err == nil { return @@ -42,7 +42,7 @@ func Error(err error) executorResultOption { } } -func ErrorString(err string) executorResultOption { +func ErrorString(err string) ExecutorResultOption { return func(er *ExecutorResult) { er.Error = err } diff --git a/internal/executors/executor_status.go b/internal/executors/executor_status.go deleted file mode 100644 index 3237281..0000000 --- a/internal/executors/executor_status.go +++ /dev/null @@ -1,22 +0,0 @@ -package executors - -type ExecutorStatus uint - -const ( - ExecutorStatus_UNKNOWN ExecutorStatus = iota - ExecutorStatus_COMPLETED - ExecutorStatus_FAILED_RETRYABLE - ExecutorStatus_FAILED_NON_RETRYABLE -) - -var ( - executorStatusNames = [...]string{"COMPLETED", "FAILED_RETRYABLE", "FAILED_NON_RETRYABLE"} -) - -func (es ExecutorStatus) String() string { - return executorStatusNames[es] -} - -func (es ExecutorStatus) Ordinal() uint { - return uint(es) -} diff --git a/internal/executors/executor_type.go b/internal/executors/executor_type.go deleted file mode 100644 index def2616..0000000 --- a/internal/executors/executor_type.go +++ /dev/null @@ -1,22 +0,0 @@ -package executors - -type ExecutorType uint - -const ( - ExecutorType_UNKNOWN ExecutorType = iota - ExecutorType_VOID - ExecutorType_HTTP - ExecutorType_SCRIPT -) - -var ( - executorTypeNames = [...]string{"VOID", "HTTP"} -) - -func (t ExecutorType) String() string { - return executorTypeNames[t] -} - -func (e ExecutorType) Ordinal() uint { - return uint(e) -} diff --git a/internal/executors/factory/executor_factory.go b/internal/executors/factory/executor_factory.go index 29e3cab..d124ce6 100644 --- a/internal/executors/factory/executor_factory.go +++ b/internal/executors/factory/executor_factory.go @@ -1,56 +1,20 @@ package factory import ( - "log" - "sync" - + "fmt" pb "github.com/SAP/remote-work-processor/build/proto/generated" "github.com/SAP/remote-work-processor/internal/executors" + "github.com/SAP/remote-work-processor/internal/executors/http" + "github.com/SAP/remote-work-processor/internal/executors/void" ) -var ( - Executor_Factory ExecutorFactory = newExecutorFactory() -) - -type ExecutorFactory struct { - generators map[pb.TaskType]ExecutorGenerator - sync.RWMutex -} - -func newExecutorFactory() ExecutorFactory { - return ExecutorFactory{ - generators: map[pb.TaskType]ExecutorGenerator{ - pb.TaskType_TASK_TYPE_VOID: voidExecutorGenerator(), - pb.TaskType_TASK_TYPE_HTTP: httpRequestExecutorGenerator(), - }, - } -} - -func (f *ExecutorFactory) Submit(t pb.TaskType, g ExecutorGenerator) *ExecutorFactory { - f.Lock() - defer f.Unlock() - - if _, e := f.generators[t]; e { - log.Fatalf("Executor of type '%s' has already been submitted in the factory", t) +func CreateExecutor(t pb.TaskType) (executors.Executor, error) { + switch t { + case pb.TaskType_TASK_TYPE_VOID: + return void.VoidExecutor{}, nil + case pb.TaskType_TASK_TYPE_HTTP: + return http.NewDefaultHttpRequestExecutor(), nil + default: + return nil, fmt.Errorf("cannot create executor of type %q", t) } - - f.generators[t] = g - return f -} - -func (f *ExecutorFactory) GetExecutor(t pb.TaskType) (executors.Executor, error) { - f.RLock() - g, ok := f.generators[t] - f.RUnlock() - - if !ok { - return nil, executors.NewExecutorCreationError(t) - } - - e, err := g() - if err != nil { - log.Fatalf("Generator failed while trying to create an executor of type '%s'", t) - } - - return e, nil } diff --git a/internal/executors/factory/executor_generator.go b/internal/executors/factory/executor_generator.go deleted file mode 100644 index 7b878cf..0000000 --- a/internal/executors/factory/executor_generator.go +++ /dev/null @@ -1,21 +0,0 @@ -package factory - -import ( - "github.com/SAP/remote-work-processor/internal/executors" - "github.com/SAP/remote-work-processor/internal/executors/http" - "github.com/SAP/remote-work-processor/internal/executors/void" -) - -type ExecutorGenerator func() (executors.Executor, error) - -func voidExecutorGenerator() ExecutorGenerator { - return func() (executors.Executor, error) { - return &void.VoidExecutor{}, nil - } -} - -func httpRequestExecutorGenerator() ExecutorGenerator { - return func() (executors.Executor, error) { - return &http.HttpRequestExecutor{}, nil - } -} diff --git a/internal/executors/http/authorization_header.go b/internal/executors/http/authorization_header.go index a15c23e..50497ce 100644 --- a/internal/executors/http/authorization_header.go +++ b/internal/executors/http/authorization_header.go @@ -1,172 +1,59 @@ package http import ( - "log" - "regexp" - "strconv" - "github.com/SAP/remote-work-processor/internal/executors" - "github.com/SAP/remote-work-processor/internal/utils/json" + "regexp" ) const ( - AUTHORIZATION_HEADER_NAME string = "Authorization" - IAS_TOKEN_URL_PATTERN string = "^https:\\/\\/(accounts\\.sap\\.com|[A-Za-z0-9+]+\\.accounts400\\.ondemand\\.com|[A-Za-z0-9+]+\\.accounts\\.ondemand\\.com)" + AuthorizationHeaderName = "Authorization" + IasTokenUrlPattern = "^https:\\/\\/(accounts\\.sap\\.com|[A-Za-z0-9+]+\\.accounts400\\.ondemand\\.com|[A-Za-z0-9+]+\\.accounts\\.ondemand\\.com)" ) -var iasTokenUrlRegex *regexp.Regexp = regexp.MustCompile(IAS_TOKEN_URL_PATTERN) - -type AuthorizationHeader interface { - GetName() string - GetValue() string - HasValue() bool -} - -type CacheableAuthorizationHeader interface { - AuthorizationHeader - GetCachingKey() string - GetCacheableValue() (string, error) - ApplyCachedToken(token string) (CacheableAuthorizationHeader, error) -} - -type AuthorizationHeaderView struct { - value string -} - -type CacheableAuthorizationHeaderView struct { - AuthorizationHeaderView - header *oAuthorizationHeader -} - -type CachedToken struct { - Token string `json:"token,omitempty"` - Timestamp string `json:"timestamp,omitempty"` -} - -func NewCacheableAuthorizationHeaderView(value string, header *oAuthorizationHeader) CacheableAuthorizationHeaderView { - return CacheableAuthorizationHeaderView{ - AuthorizationHeaderView: AuthorizationHeaderView{ - value: value, - }, - header: header, - } -} - -func (h CacheableAuthorizationHeaderView) GetCachingKey() string { - return h.header.cachingKey -} - -func (h CacheableAuthorizationHeaderView) GetCacheableValue() (string, error) { - token := h.header.token - if token == nil { - return "", nil - } - - t, err := json.ToJson(token) - if err != nil { - return "", err - } - - cached := CachedToken{ - Token: t, - Timestamp: strconv.FormatInt(token.issuedAt, 10), - } - - value, err := json.ToJson(cached) - if err != nil { - return "", err - } - - return string(value), nil -} - -func (h CacheableAuthorizationHeaderView) ApplyCachedToken(token string) (CacheableAuthorizationHeader, error) { - if token == "" { - return h, nil - } - - cached := &CachedToken{} - err := json.FromJson(token, cached) - if err != nil { - return nil, err - } - - if cached.Token == "" || cached.Timestamp == "" { - return h, nil - } - - issuedAt, err := strconv.ParseInt(cached.Timestamp, 10, 64) - if err != nil { - return nil, err - } - - h.header.setToken(cached.Token, issuedAt) - return nil, nil -} +var iasTokenUrlRegex = regexp.MustCompile(IasTokenUrlPattern) -func EmptyAuthorizationHeader() AuthorizationHeaderView { - return AuthorizationHeaderView{} -} - -func NewAuthorizationHeaderView(value string) AuthorizationHeaderView { - return AuthorizationHeaderView{ - value: value, - } -} +// Currently only Basic and Bearer token authentication is supported. +// OAuth 2.0 will be added later -func (h AuthorizationHeaderView) GetName() string { - return AUTHORIZATION_HEADER_NAME -} - -func (h AuthorizationHeaderView) GetValue() string { - return h.value -} +func CreateAuthorizationHeader(params *HttpRequestParameters) (string, error) { + authHeader := params.GetAuthorizationHeader() -func (h AuthorizationHeaderView) HasValue() bool { - return h.value != "" -} - -// Currently Basic authentication and Bearer token authentication is supported, OAuth 2.0 will be added later -func CreateAuthorizationHeader(params *HttpRequestParameters) (AuthorizationHeader, error) { - extH := params.GetAuthorizationHeader() - - if extH != "" { - return NewExternalAuthorizationHeader(extH).Generate() + if authHeader != "" { + return authHeader, nil } - u := params.GetUser() - p := params.GetPassword() + user := params.GetUser() + pass := params.GetPassword() tokenUrl := params.GetTokenUrl() if tokenUrl != "" { - if u != "" && iasTokenUrlRegex.Match([]byte(tokenUrl)) { - return NewIasAuthorizationHeader(tokenUrl, u, params.GetCertificateAuthentication().GetClientCertificate()).Generate() + if user != "" && iasTokenUrlRegex.Match([]byte(tokenUrl)) { + return NewIasAuthorizationHeader(tokenUrl, user, params.GetCertificateAuthentication().GetClientCertificate()).Generate() } - - return NewOAuthHeaderGenerator(params).Generate() + return NewOAuthHeaderGenerator(params).GenerateWithCacheAside() } - if u != "" { - return NewBasicAuthorizationHeader(u, p).Generate() + if user != "" { + return NewBasicAuthorizationHeader(user, pass).Generate() } if noAuthorizationRequired(params) { - log.Printf("Request does not need any type of authorization header") - return EmptyAuthorizationHeader(), nil + return "", nil } - return EmptyAuthorizationHeader(), executors.NewNonRetryableError("Input values for the authentication related keys (user, password & authorizationHeader) are not combined properly.") + return "", executors.NewNonRetryableError("Input values for the authentication-related keys " + + "(user, password & authorizationHeader) are not combined properly.") } func noAuthorizationRequired(p *HttpRequestParameters) bool { - switch "" { - case p.authorizationHeader, - p.tokenUrl, - p.clientId, - p.user, - p.refreshToken: - return true - default: + isEmpty := func(s string) bool { return len(s) == 0 } + isAnyEmpty := func(strings ...string) bool { + for _, s := range strings { + if isEmpty(s) { + return true + } + } return false } + return isAnyEmpty(p.authorizationHeader, p.tokenUrl, p.clientId, p.user, p.refreshToken) } diff --git a/internal/executors/http/basic_authorization_header.go b/internal/executors/http/basic_authorization_header.go index 197ff7f..444e2d6 100644 --- a/internal/executors/http/basic_authorization_header.go +++ b/internal/executors/http/basic_authorization_header.go @@ -17,8 +17,9 @@ func NewBasicAuthorizationHeader(u string, p string) AuthorizationHeaderGenerato } } -func (h *basicAuthorizationHeader) Generate() (AuthorizationHeader, error) { - c := fmt.Sprintf("%s:%s", h.username, h.password) - - return NewAuthorizationHeaderView(fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(c)))), nil +func (h *basicAuthorizationHeader) Generate() (string, error) { + encoded := base64.StdEncoding.EncodeToString( + fmt.Appendf(nil, "%s:%s", h.username, h.password), + ) + return fmt.Sprintf("Basic %s", encoded), nil } diff --git a/internal/executors/http/csrf_token_fetcher.go b/internal/executors/http/csrf_token_fetcher.go index 316dcad..d04b801 100644 --- a/internal/executors/http/csrf_token_fetcher.go +++ b/internal/executors/http/csrf_token_fetcher.go @@ -1,17 +1,14 @@ package http import ( + "fmt" + "github.com/SAP/remote-work-processor/internal/utils" "net/http" - - "github.com/SAP/remote-work-processor/internal/functional" - "github.com/SAP/remote-work-processor/internal/utils/array" - "github.com/SAP/remote-work-processor/internal/utils/maps" - "github.com/SAP/remote-work-processor/internal/utils/tuple" ) -const CSRF_VERB = "fetch" +const CsrfVerb = "fetch" -var csrfTokenHeaders = [...]string{"X-Csrf-Token", "X-Xsrf-Token"} +var csrfTokenHeaders = []string{"X-Csrf-Token", "X-Xsrf-Token"} type csrfTokenFetcher struct { HttpExecutor @@ -20,57 +17,43 @@ type csrfTokenFetcher struct { succeedOnTimeout bool } -func NewCsrfTokenFetcher(p *HttpRequestParameters, authHeader AuthorizationHeader) TokenFetcher { +func NewCsrfTokenFetcher(p *HttpRequestParameters, authHeader string) TokenFetcher { return &csrfTokenFetcher{ - HttpExecutor: NewHttpRequestExecutor(authHeader), + HttpExecutor: NewDefaultHttpRequestExecutor(), csrfUrl: p.csrfUrl, - headers: createCsrfHeaders(p.headers, authHeader), + headers: createCsrfHeaders(authHeader), succeedOnTimeout: p.succeedOnTimeout, } } func (f *csrfTokenFetcher) Fetch() (string, error) { - p := f.createRequestParameters() + params, _ := f.createRequestParameters() - r, err := f.HttpExecutor.ExecuteWithParameters(p) + resp, err := f.HttpExecutor.ExecuteWithParameters(params) if err != nil { return "", err } - pairs := maps.Pairs(r.Headers) - filtered := array.Filter(pairs, func(pair tuple.Pair[string, string]) bool { - // TODO: Optimize - return array.Contains(csrfTokenHeaders[:], pair.Key) - }) - - // TODO: Error handling - - return filtered[0].Value, nil + for key, value := range resp.Headers { + if utils.Contains(csrfTokenHeaders, key) { + return value, nil + } + } + return "", fmt.Errorf("no csrf header present in response from %s", f.csrfUrl) } -func createCsrfHeaders(headers HttpHeaders, authHeader AuthorizationHeader) HttpHeaders { - pairs := array.Map(csrfTokenHeaders[:], func(header string) tuple.Pair[string, string] { - return tuple.PairOf(header, CSRF_VERB) - }) - - csrfHeaders := map[string]string{} - for _, p := range pairs { - csrfHeaders[p.Key] = p.Value +func createCsrfHeaders(authHeader string) HttpHeaders { + csrfHeaders := make(map[string]string) + for _, headerKey := range csrfTokenHeaders { + csrfHeaders[headerKey] = CsrfVerb } - if authHeader.HasValue() { - csrfHeaders[authHeader.GetName()] = authHeader.GetValue() + if authHeader != "" { + csrfHeaders[AuthorizationHeaderName] = authHeader } - return csrfHeaders } -func (f *csrfTokenFetcher) createRequestParameters() *HttpRequestParameters { - opts := []functional.OptionWithError[HttpRequestParameters]{ - WithUrl(f.csrfUrl), - WithMethod(http.MethodGet), - WithHeaders(f.headers), - } - - return NewHttpRequestParameters(opts...) +func (f *csrfTokenFetcher) createRequestParameters() (*HttpRequestParameters, error) { + return NewHttpRequestParameters(http.MethodGet, f.csrfUrl, WithHeaders(f.headers)) } diff --git a/internal/executors/http/errors.go b/internal/executors/http/errors.go index b958cbe..6dc6e8d 100644 --- a/internal/executors/http/errors.go +++ b/internal/executors/http/errors.go @@ -2,10 +2,6 @@ package http import "fmt" -const ( - INVALID_OAUTH_TOKEN_ERROR_MESSAGE = "Invalid oAuth 2.0 token response.\nURL: %s\nMethod: %s\nResponse code: %s" -) - type IllegalTokenTypeError struct { tokenType TokenType } @@ -17,23 +13,5 @@ func NewIllegalTokenTypeError(tokenType TokenType) *IllegalTokenTypeError { } func (e *IllegalTokenTypeError) Error() string { - return fmt.Sprintf("Invalid value for token type '%s'", e.tokenType) -} - -type OAuthTokenParseError struct { - url string - method string - responseCode string -} - -func NewOAuthTokenParseError(url string, method string, responseCode string) *OAuthTokenParseError { - return &OAuthTokenParseError{ - url: url, - method: method, - responseCode: responseCode, - } -} - -func (e *OAuthTokenParseError) Error() string { - return fmt.Sprintf(INVALID_OAUTH_TOKEN_ERROR_MESSAGE, e.url, e.method, e.responseCode) + return fmt.Sprintf("invalid value for token type %q", e.tokenType) } diff --git a/internal/executors/http/external_authorization_header.go b/internal/executors/http/external_authorization_header.go deleted file mode 100644 index ff74cd9..0000000 --- a/internal/executors/http/external_authorization_header.go +++ /dev/null @@ -1,15 +0,0 @@ -package http - -type externalAuthorizationHeader struct { - value string -} - -func NewExternalAuthorizationHeader(v string) AuthorizationHeaderGenerator { - return &externalAuthorizationHeader{ - value: v, - } -} - -func (h *externalAuthorizationHeader) Generate() (AuthorizationHeader, error) { - return NewAuthorizationHeaderView(h.value), nil -} diff --git a/internal/executors/http/generator.go b/internal/executors/http/generator.go index 9f7b354..17d149a 100644 --- a/internal/executors/http/generator.go +++ b/internal/executors/http/generator.go @@ -1,5 +1,10 @@ package http type AuthorizationHeaderGenerator interface { - Generate() (AuthorizationHeader, error) + Generate() (string, error) +} + +type CacheableAuthorizationHeaderGenerator interface { + AuthorizationHeaderGenerator + GenerateWithCacheAside() (string, error) } diff --git a/internal/executors/http/grant_type.go b/internal/executors/http/grant_type.go index b3f8ef5..eb06477 100644 --- a/internal/executors/http/grant_type.go +++ b/internal/executors/http/grant_type.go @@ -15,7 +15,3 @@ var ( func (t GrantType) String() string { return grantTypeNames[t] } - -func (e GrantType) Ordinal() uint { - return uint(e) -} diff --git a/internal/executors/http/http_client.go b/internal/executors/http/http_client.go index 4f0fa65..fbc207c 100644 --- a/internal/executors/http/http_client.go +++ b/internal/executors/http/http_client.go @@ -8,27 +8,27 @@ import ( ) const ( - DEFAULT_HTTP_REQUEST_TIMEOUT_IN_S = 3 * time.Second + DefaultHttpRequestTimeout = 3 * time.Second ) -func CreateHttpClient(timeoutInS uint64, certAuth *tls.CertificateAuthentication) (http.Client, error) { +func CreateHttpClient(timeoutInS uint64, certAuth *tls.CertificateAuthentication) (*http.Client, error) { var tp http.RoundTripper if certAuth != nil { var err error tp, err = tls.NewTLSConfigurationProvider(certAuth).CreateTransport() if err != nil { - return http.Client{}, err + return nil, err } } - c := http.Client{ + c := &http.Client{ CheckRedirect: doNotFollowRedirects(), Transport: tp, } if timeoutInS == 0 { - c.Timeout = DEFAULT_HTTP_REQUEST_TIMEOUT_IN_S + c.Timeout = DefaultHttpRequestTimeout } else { c.Timeout = time.Duration(timeoutInS) * time.Second } diff --git a/internal/executors/http/http_executor.go b/internal/executors/http/http_executor.go index f8ac1de..d67a668 100644 --- a/internal/executors/http/http_executor.go +++ b/internal/executors/http/http_executor.go @@ -1,7 +1,6 @@ package http import ( - "bytes" "errors" "fmt" "io" @@ -10,61 +9,50 @@ import ( "net/http" "net/http/httptrace" "strconv" + "strings" "time" pb "github.com/SAP/remote-work-processor/build/proto/generated" - "github.com/SAP/remote-work-processor/internal/cache" "github.com/SAP/remote-work-processor/internal/executors" ) type HttpExecutor interface { - ExecuteWithParameters(p *HttpRequestParameters) (HttpResponse, error) + ExecuteWithParameters(*HttpRequestParameters) (*HttpResponse, error) } type HttpRequestExecutor struct { executors.Executor - authorizationHeader AuthorizationHeader - store cache.MapCache[string, string] } -func NewHttpRequestExecutor(h AuthorizationHeader) *HttpRequestExecutor { - return &HttpRequestExecutor{ - authorizationHeader: h, - } +func NewDefaultHttpRequestExecutor() *HttpRequestExecutor { + return &HttpRequestExecutor{} } -func DefaultHttpRequestExecutor() *HttpRequestExecutor { - return &HttpRequestExecutor{ - authorizationHeader: AuthorizationHeaderView{}, +func (e *HttpRequestExecutor) Execute(ctx executors.Context) *executors.ExecutorResult { + log.Println("Executing HttpRequest command...") + params, err := NewHttpRequestParametersFromContext(ctx) + if err != nil { + return executors.NewExecutorResult( + executors.Status(pb.TaskExecutionResponseMessage_TASK_STATE_FAILED_NON_RETRYABLE), + executors.Error(err), + ) } -} - -func (e *HttpRequestExecutor) Execute(ctx executors.ExecutorContext) *executors.ExecutorResult { - p := NewHttpRequestParametersFromContext(ctx) - e.store = ctx.GetStore() - resp, err := e.ExecuteWithParameters(p) + resp, err := e.ExecuteWithParameters(params) - switch e := err.(type) { + switch typedErr := err.(type) { case *executors.RetryableError: return executors.NewExecutorResult( executors.Status(pb.TaskExecutionResponseMessage_TASK_STATE_FAILED_RETRYABLE), - executors.Error(e), + executors.Error(typedErr), ) case *executors.NonRetryableError: return executors.NewExecutorResult( executors.Status(pb.TaskExecutionResponseMessage_TASK_STATE_FAILED_NON_RETRYABLE), - executors.Error(e), + executors.Error(typedErr), ) default: - m, err := resp.ToMap() - if (errors.Is(&executors.NonRetryableError{}, err)) { - return executors.NewExecutorResult( - executors.Status(pb.TaskExecutionResponseMessage_TASK_STATE_FAILED_NON_RETRYABLE), - executors.Error(err), - ) - } - + m := resp.ToMap() if !resp.successful { return executors.NewExecutorResult( executors.Output(m), @@ -80,110 +68,60 @@ func (e *HttpRequestExecutor) Execute(ctx executors.ExecutorContext) *executors. } } -func (e *HttpRequestExecutor) ExecuteWithParameters(p *HttpRequestParameters) (HttpResponse, error) { - c, err := CreateHttpClient(p.timeout, p.certAuthentication) +func (e *HttpRequestExecutor) ExecuteWithParameters(p *HttpRequestParameters) (*HttpResponse, error) { + client, err := CreateHttpClient(p.timeout, p.certAuthentication) if err != nil { - return HttpResponse{}, err + return nil, err } - var authHeader AuthorizationHeader = e.authorizationHeader - if e.authorizationHeader == nil { - authHeader, err = CreateAuthorizationHeader(p) - if err != nil { - return HttpResponse{}, err - } + authHeader, err := CreateAuthorizationHeader(p) + if err != nil { + return nil, err } - e.applyTokenIfCached(authHeader) - if p.csrfUrl != "" { - if err := obtainCsrf(p, authHeader); err != nil { - return HttpResponse{}, err + if err = obtainCsrf(p, authHeader); err != nil { + return nil, err } } - - resp, err := execute(c, p, authHeader) - if err != nil { - return HttpResponse{}, err - } - - err = e.cacheToken(authHeader) - if err != nil { - return HttpResponse{}, err - } - - return resp, nil + return execute(client, p, authHeader) } -func obtainCsrf(p *HttpRequestParameters, authHeader AuthorizationHeader) error { +func obtainCsrf(p *HttpRequestParameters, authHeader string) error { fetcher := NewCsrfTokenFetcher(p, authHeader) token, err := fetcher.Fetch() if err != nil { - return err + return fmt.Errorf("failed to fetch CSRF token: %v", err) } p.headers[csrfTokenHeaders[0]] = token return nil } -func (e *HttpRequestExecutor) cacheToken(header AuthorizationHeader) error { - h, ok := header.(CacheableAuthorizationHeader) - if !ok { - return nil - } - - key := h.GetCachingKey() - value, err := h.GetCacheableValue() +func execute(c *http.Client, p *HttpRequestParameters, authHeader string) (*HttpResponse, error) { + req, timeCh, err := createRequest(p.method, p.url, p.headers, p.body, authHeader) if err != nil { - return err - } - - if value == "" { - return nil - } - - e.store.Write(key, value) - return nil -} - -func (e *HttpRequestExecutor) applyTokenIfCached(header AuthorizationHeader) { - h, ok := header.(CacheableAuthorizationHeader) - if !ok { - return + return nil, executors.NewNonRetryableError("could not create http request: %v", err).WithCause(err) } - log.Printf("Applying 'http' executable's cache for cacheable header. Cache size is: %d", e.store.Size()) - cached := e.store.Read(h.GetCachingKey()) - if cached == "" { - return - } - - h.ApplyCachedToken(cached) -} - -func execute(c http.Client, p *HttpRequestParameters, authHeader AuthorizationHeader) (HttpResponse, error) { - reqCh, timeCh := createRequest(p.method, p.url, p.headers, p.body, authHeader) - req := <-reqCh - + log.Printf("Executing request %s %s...\n", p.method, p.url) resp, err := c.Do(req) if requestTimedOut(err) { if p.succeedOnTimeout { - r, _ := newTimedOutHttpResponse(req, resp) - - return *r, nil + return newTimedOutHttpResponse(req, resp) } - return HttpResponse{}, executors.NewRetryableError(fmt.Sprintf("Http request timed out after %d seconds", p.timeout)).WithCause(err) + return nil, executors.NewRetryableError("HTTP request timed out after %d seconds", p.timeout).WithCause(err) } if err != nil { - return HttpResponse{}, executors.NewNonRetryableError(fmt.Sprintf("Error occurred while trying to execute actual HTTP request: %v\n", err)).WithCause(err) + return nil, executors.NewNonRetryableError("Error occurred while trying to execute actual HTTP request: %v", err).WithCause(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return HttpResponse{}, executors.NewNonRetryableError(fmt.Sprintf("Error occurred while trying to read HTTP response body: %v\n", err)).WithCause(err) + return nil, executors.NewNonRetryableError("Error occurred while trying to read HTTP response body: %v", err).WithCause(err) } r, err := NewHttpResponse( @@ -197,90 +135,51 @@ func execute(c http.Client, p *HttpRequestParameters, authHeader AuthorizationHe Time(<-timeCh), ) if err != nil { - return HttpResponse{}, executors.NewNonRetryableError(fmt.Sprintf("Error occurred while trying to build HTTP response: %v\n", err)).WithCause(err) + return nil, executors.NewNonRetryableError("Error occurred while trying to build HTTP response: %v", err).WithCause(err) } - return *r, nil + return r, nil } func requestTimedOut(err error) bool { - if err == nil { - return false - } - var e net.Error - if errors.As(err, &e); e.Timeout() { - return true - } - - return false + return errors.As(err, &e) && e.Timeout() } -func createRequest(method string, url string, headers map[string]string, body string, authHeader AuthorizationHeader) (<-chan *http.Request, <-chan int64) { +func createRequest(method string, url string, headers map[string]string, body, authHeader string) (*http.Request, <-chan int64, error) { timeCh := make(chan int64, 1) - reqCh := make(chan *http.Request) - - go func() { - m, _ := resolveMethod(method) - req, _ := http.NewRequest(m, url, bytes.NewBuffer([]byte(body))) - - var start time.Time - trace := &httptrace.ClientTrace{ - ConnectStart: func(_, __ string) { - start = time.Now() - }, - GotFirstResponseByte: func() { - ms := time.Since(start).Milliseconds() - fmt.Printf("HTTP Request Time: %d", ms) - timeCh <- ms - fmt.Printf("HTTP Request time has been sent.") - }, - } - - req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) - - addHeaders(req, headers, authHeader) + req, err := http.NewRequest(method, url, strings.NewReader(body)) + if err != nil { + return nil, nil, err + } + addHeaders(req, headers, authHeader) - fmt.Printf("Built request is going to be sent to channel....") - reqCh <- req - }() + var start time.Time + trace := &httptrace.ClientTrace{ + ConnectStart: func(_, _ string) { + start = time.Now() + }, + GotFirstResponseByte: func() { + timeCh <- time.Since(start).Milliseconds() + }, + } + traceCtx := httptrace.WithClientTrace(req.Context(), trace) - return reqCh, timeCh + return req.WithContext(traceCtx), timeCh, nil } -func addHeaders(req *http.Request, headers map[string]string, authHeader AuthorizationHeader) { +func addHeaders(req *http.Request, headers map[string]string, authHeader string) { for k, v := range headers { req.Header.Add(k, v) } - if authHeader.HasValue() { - req.Header.Set(authHeader.GetName(), authHeader.GetValue()) - } -} - -func resolveMethod(m string) (string, error) { - switch m { - case http.MethodHead: - return http.MethodHead, nil - case http.MethodGet: - return http.MethodGet, nil - case http.MethodPost: - return http.MethodPost, nil - case http.MethodPut: - return http.MethodPut, nil - case http.MethodPatch: - return http.MethodPatch, nil - case http.MethodDelete: - return http.MethodDelete, nil - case http.MethodOptions: - return http.MethodOptions, nil - default: - return "", executors.NewInvalidHttpMethodError(m) + if authHeader != "" { + req.Header.Set(AuthorizationHeaderName, authHeader) } } -func buildHttpError(resp HttpResponse) string { +func buildHttpError(resp *HttpResponse) string { code, _ := strconv.Atoi(resp.StatusCode) return fmt.Sprintf("HTTP request failed\nReason: %s\nURL: %s\nMethod: %s\nResponse code: %s", http.StatusText(code), resp.Url, resp.Method, resp.StatusCode) diff --git a/internal/executors/http/http_executor_parameters.go b/internal/executors/http/http_executor_parameters.go index 339d90e..7bcff11 100644 --- a/internal/executors/http/http_executor_parameters.go +++ b/internal/executors/http/http_executor_parameters.go @@ -28,9 +28,7 @@ const ( AUTHORIZATION_HEADER string = "authorizationHeader" ) -var ( - defaultSuccessResponseCodes [1]string = [...]string{"2xx"} -) +var defaultSuccessResponseCodes = []string{"2xx"} type HttpRequestParameters struct { method string @@ -50,42 +48,54 @@ type HttpRequestParameters struct { succeedOnTimeout bool certAuthentication *tls.CertificateAuthentication authorizationHeader string + + store map[string]string } -func NewHttpRequestParametersFromContext(ctx executors.ExecutorContext) *HttpRequestParameters { - opts := []functional.OptionWithError[HttpRequestParameters]{ - withMethodFromContext(&ctx), - withUrlFromContext(&ctx), - withTokenUrlFromContext(&ctx), - withCsrfUrlFromContext(&ctx), - withClientIdFromContext(&ctx), - withClientSecretFromContext(&ctx), - withRefreshTokenFromContext(&ctx), - withResponseBodyTransformerFromContext(&ctx), - withHeadersFromContext(&ctx), - withBodyFromContext(&ctx), - withUserFromContext(&ctx), - withPasswordFromContext(&ctx), - withTimeoutFromContext(&ctx), - withSuccessResponseCodesFromContext(&ctx), - withSucceedOnTimeoutFromContext(&ctx), - withCertAuthenticationFromContext(&ctx), - withAuthorizationHeaderFromContext(&ctx), +func NewHttpRequestParametersFromContext(ctx executors.Context) (*HttpRequestParameters, error) { + method, err := ctx.GetRequiredString(METHOD) + if err != nil { + return nil, nonRetryableError(err) } - return applyBuildOptions(&HttpRequestParameters{}, opts...) -} + url, err := ctx.GetRequiredString(URL) + if err != nil { + return nil, nonRetryableError(err) + } -func NewHttpRequestParameters(opts ...functional.OptionWithError[HttpRequestParameters]) *HttpRequestParameters { - return applyBuildOptions(&HttpRequestParameters{}, opts...) -} + opts := []functional.OptionWithError[HttpRequestParameters]{ + withTokenUrlFromContext(ctx), + withCsrfUrlFromContext(ctx), + withClientIdFromContext(ctx), + withClientSecretFromContext(ctx), + withRefreshTokenFromContext(ctx), + withResponseBodyTransformerFromContext(ctx), + withHeadersFromContext(ctx), + withBodyFromContext(ctx), + withUserFromContext(ctx), + withPasswordFromContext(ctx), + withTimeoutFromContext(ctx), + withSuccessResponseCodesFromContext(ctx), + withSucceedOnTimeoutFromContext(ctx), + withCertAuthenticationFromContext(ctx), + withAuthorizationHeaderFromContext(ctx), + withStoreFromContext(ctx), + } + return NewHttpRequestParameters(method, url, opts...) +} + +func NewHttpRequestParameters(method, url string, opts ...functional.OptionWithError[HttpRequestParameters]) (*HttpRequestParameters, error) { + p := &HttpRequestParameters{ + method: method, + url: url, + } -func applyBuildOptions(p *HttpRequestParameters, opts ...functional.OptionWithError[HttpRequestParameters]) *HttpRequestParameters { for _, opt := range opts { - opt(p) + if err := opt(p); err != nil { + return nil, err + } } - - return p + return p, nil } func (p HttpRequestParameters) GetTokenUrl() string { @@ -124,299 +134,260 @@ func (p HttpRequestParameters) GetCertificateAuthentication() *tls.CertificateAu return p.certAuthentication } -func WithMethod(m string) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.method = m - - return nil - } -} - -func WithUrl(u string) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.url = u - - return nil - } -} - func WithTokenUrl(u string) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.tokenUrl = u + return func(params *HttpRequestParameters) error { + params.tokenUrl = u return nil } } func WithCsrfUrl(u string) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.csrfUrl = u + return func(params *HttpRequestParameters) error { + params.csrfUrl = u return nil } } func WithClientId(id string) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.clientId = id + return func(params *HttpRequestParameters) error { + params.clientId = id return nil } } func WithClientSecret(s string) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.clientSecret = s + return func(params *HttpRequestParameters) error { + params.clientSecret = s return nil } } func WithRefreshToken(rt string) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.refreshToken = rt + return func(params *HttpRequestParameters) error { + params.refreshToken = rt return nil } } func WithHeaders(h map[string]string) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.headers = h + return func(params *HttpRequestParameters) error { + params.headers = h return nil } } func WithBody(b string) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.body = b + return func(params *HttpRequestParameters) error { + params.body = b return nil } } func WithUser(u string) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.user = u + return func(params *HttpRequestParameters) error { + params.user = u return nil } } func WithPassword(p string) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.password = p + return func(params *HttpRequestParameters) error { + params.password = p return nil } } func WithTimeout(t uint64) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.timeout = t + return func(params *HttpRequestParameters) error { + params.timeout = t return nil } } func WithSuccessResponseCodes(src []string) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.successResponseCodes = src + return func(params *HttpRequestParameters) error { + params.successResponseCodes = src return nil } } func WithSucceedOnTimeout(s bool) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.succeedOnTimeout = s + return func(params *HttpRequestParameters) error { + params.succeedOnTimeout = s return nil } } func WithCertificateAuthentication(cauth *tls.CertificateAuthentication) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.certAuthentication = cauth + return func(params *HttpRequestParameters) error { + params.certAuthentication = cauth return nil } } func WithAuthorizationHeader(h string) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - hrp.authorizationHeader = h + return func(params *HttpRequestParameters) error { + params.authorizationHeader = h return nil } } -func withMethodFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - m, err := ctx.GetRequiredString(METHOD) - if err != nil { - return nonRetryableError(err) - } - - hrp.method = m - return nil - } -} - -func withUrlFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - u, err := ctx.GetRequiredString(URL) - if err != nil { - nonRetryableError(err) - } - - hrp.url = u - return nil - } -} - -func withTokenUrlFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { +func withTokenUrlFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { u := ctx.GetString(TOKEN_URL) - hrp.tokenUrl = u + params.tokenUrl = u return nil } } -func withCsrfUrlFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { +func withCsrfUrlFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { u := ctx.GetString(CSRF_URL) - hrp.csrfUrl = u + params.csrfUrl = u return nil } } -func withClientIdFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { +func withClientIdFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { id := ctx.GetString(CLIENT_ID) - hrp.clientId = id + params.clientId = id return nil } } -func withClientSecretFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { +func withClientSecretFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { s := ctx.GetString(CLIENT_SECRET) - hrp.clientSecret = s + params.clientSecret = s return nil } } -func withRefreshTokenFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { +func withRefreshTokenFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { rt := ctx.GetString(REFRESH_TOKEN) - hrp.refreshToken = rt + params.refreshToken = rt return nil } } -func withResponseBodyTransformerFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { +func withResponseBodyTransformerFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { t := ctx.GetString(RESPONSE_BODY_TRANSFORMER) - hrp.responseBodyTransformer = t + params.responseBodyTransformer = t return nil } } -func withHeadersFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { +func withHeadersFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { h, err := ctx.GetMap(HEADERS) if err != nil { - nonRetryableError(err) + return nonRetryableError(err) } - hrp.headers = h + params.headers = h return nil } } -func withBodyFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { +func withBodyFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { b := ctx.GetString(BODY) - hrp.body = b + params.body = b return nil } } -func withUserFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { +func withUserFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { u := ctx.GetString(USER) - hrp.user = u + params.user = u return nil } } -func withPasswordFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { +func withPasswordFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { p := ctx.GetString(PASSWORD) - hrp.password = p + params.password = p return nil } } -func withTimeoutFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - t, err := ctx.GetNumber(TIMEOUT) +func withTimeoutFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { + timeout, err := ctx.GetNumber(TIMEOUT) if err != nil { return nonRetryableError(err) } - hrp.timeout = t + params.timeout = timeout return nil } } -func withSuccessResponseCodesFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { +func withSuccessResponseCodesFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { src, err := ctx.GetList(SUCCESS_RESPONSE_CODES) if err != nil { return nonRetryableError(err) } if len(src) == 0 { - hrp.successResponseCodes = defaultSuccessResponseCodes[:] + params.successResponseCodes = defaultSuccessResponseCodes } else { - hrp.successResponseCodes = src + params.successResponseCodes = src } return nil } } -func withSucceedOnTimeoutFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { +func withSucceedOnTimeoutFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { s, err := ctx.GetBoolean(SUCCEED_ON_TIMEOUT) if err != nil { return nonRetryableError(err) } - hrp.succeedOnTimeout = s + params.succeedOnTimeout = s return nil } } -func withCertAuthenticationFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { - opts := []tls.CertificateAuthenticationOption{} +func withCertAuthenticationFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { + var opts []tls.CertificateAuthenticationOption tCerts := ctx.GetString(TRUSTED_CERTS) if len(tCerts) > 0 { opts = append(opts, tls.TrustCertificates(tCerts)) } + cCert := ctx.GetString(CLIENT_CERT) if len(cCert) > 0 { opts = append(opts, tls.WithClientCertificate(cCert)) @@ -429,16 +400,23 @@ func withCertAuthenticationFromContext(ctx *executors.ExecutorContext) functiona opts = append(opts, tls.TrustAnyCertificate(trustAnyCert)) // TODO: Validation can be done before creating CertificateAuthentication object - hrp.certAuthentication = tls.NewCertAuthentication(opts...) + params.certAuthentication = tls.NewCertAuthentication(opts...) return nil } } -func withAuthorizationHeaderFromContext(ctx *executors.ExecutorContext) functional.OptionWithError[HttpRequestParameters] { - return func(hrp *HttpRequestParameters) error { +func withAuthorizationHeaderFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { h := ctx.GetString(AUTHORIZATION_HEADER) - hrp.authorizationHeader = h + params.authorizationHeader = h + return nil + } +} + +func withStoreFromContext(ctx executors.Context) functional.OptionWithError[HttpRequestParameters] { + return func(params *HttpRequestParameters) error { + params.store = ctx.GetStore() return nil } } diff --git a/internal/executors/http/http_response.go b/internal/executors/http/http_response.go index cccdee4..b6b84ff 100644 --- a/internal/executors/http/http_response.go +++ b/internal/executors/http/http_response.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "reflect" "strconv" "strings" @@ -19,10 +20,11 @@ type HttpResponse struct { Content string `json:"body"` Headers HttpHeaders `json:"headers"` StatusCode string `json:"status"` - SizeInBytes uint `json:"size"` + SizeInBytes uint64 `json:"size"` Time int64 `json:"time"` ResponseBodyTransformer string `json:"responseBodyTransformer"` - successful bool + + successful bool } func NewHttpResponse(opts ...functional.OptionWithError[HttpResponse]) (*HttpResponse, error) { @@ -44,7 +46,6 @@ func newTimedOutHttpResponse(req *http.Request, resp *http.Response) (*HttpRespo opts := []functional.OptionWithError[HttpResponse]{ Url(req.URL.String()), Method(req.Method), - Content(""), StatusCode(-1), } @@ -74,7 +75,7 @@ func Method(method string) functional.OptionWithError[HttpResponse] { func Content(body string) functional.OptionWithError[HttpResponse] { return func(hr *HttpResponse) error { hr.Content = body - hr.SizeInBytes = uint(len(body)) + hr.SizeInBytes = uint64(len(body)) return nil } @@ -116,9 +117,9 @@ func ResponseBodyTransformer(transformer string) functional.OptionWithError[Http func IsSuccessfulBasedOnSuccessResponseCodes(statusCode int, successResponseCodes []string) functional.OptionWithError[HttpResponse] { return func(hr *HttpResponse) error { - isSuccessful, err4 := isSuccessfulResponseCode(uint16(statusCode), successResponseCodes...) - if err4 != nil { - return executors.NewNonRetryableError(fmt.Sprintf("Error occurred while trying to resolve success exit codes values: %v\n", err4)).WithCause(err4) + isSuccessful, err := isSuccessfulResponseCode(statusCode, successResponseCodes...) + if err != nil { + return executors.NewNonRetryableError("Error occurred while trying to resolve success exit codes values: %v", err).WithCause(err) } hr.successful = isSuccessful @@ -126,10 +127,10 @@ func IsSuccessfulBasedOnSuccessResponseCodes(statusCode int, successResponseCode } } -func isSuccessfulResponseCode(statusCode uint16, successResponseCodes ...string) (bool, error) { - codes, err2 := parseSuccessResponseCodes(successResponseCodes...) - if err2 != nil { - return false, err2 +func isSuccessfulResponseCode(statusCode int, successResponseCodes ...string) (bool, error) { + codes, err := parseSuccessResponseCodes(successResponseCodes...) + if err != nil { + return false, err } for _, code := range codes { @@ -137,50 +138,57 @@ func isSuccessfulResponseCode(statusCode uint16, successResponseCodes ...string) return true, nil } } - return false, nil } -func parseSuccessResponseCodes(successResponseCodes ...string) ([]uint16, error) { - parsed := []uint16{} +func parseSuccessResponseCodes(successResponseCodes ...string) ([]int, error) { + var parsed []int for _, code := range successResponseCodes { c := code if strings.Contains(code, "x") { c = code[0:1] } - u, err := parseUint(c) + intCode, err := strconv.Atoi(c) if err != nil { return nil, err } - parsed = append(parsed, u) + parsed = append(parsed, intCode) } - return parsed, nil } -func parseUint(v string) (uint16, error) { - u, err := strconv.ParseUint(v, 10, 16) - if err != nil { - return 0, err - } +func (r HttpResponse) ToMap() map[string]string { + rtype := reflect.TypeOf(r) + rvalue := reflect.ValueOf(r) + result := make(map[string]string, rtype.NumField()) - return uint16(u), nil -} + for i := 0; i < rtype.NumField(); i++ { + fieldType := rtype.Field(i) + if !fieldType.IsExported() { + continue + } -// TODO: Implementation can be improved with reflection and removing json marshalling-unmarshalling process -func (r HttpResponse) ToMap() (map[string]interface{}, error) { - b, err := json.Marshal(r) - if err != nil { - return nil, executors.NewNonRetryableError("Failed to marshal HttpResponse into JSON encoded object").WithCause(err) + field := rvalue.Field(i) + jsonKey := fieldType.Tag.Get("json") + + switch field.Kind() { + case reflect.String: + result[jsonKey] = field.String() + case reflect.Uint64: + result[jsonKey] = strconv.FormatUint(field.Uint(), 10) + case reflect.Int64: + result[jsonKey] = strconv.FormatInt(field.Int(), 10) + default: + result[jsonKey] = field.Interface().(fmt.Stringer).String() + } } - m := make(map[string]interface{}) - err = json.Unmarshal(b, &m) - if err != nil { - return nil, executors.NewNonRetryableError("Failed to build HttpResponse values").WithCause(err) - } + return result +} - return m, nil +func (h HttpHeaders) String() string { + bytes, _ := json.Marshal(h) + return string(bytes) } diff --git a/internal/executors/http/ias_authorization_header.go b/internal/executors/http/ias_authorization_header.go index b799e55..37e29d4 100644 --- a/internal/executors/http/ias_authorization_header.go +++ b/internal/executors/http/ias_authorization_header.go @@ -2,8 +2,7 @@ package http import ( "fmt" - - "github.com/SAP/remote-work-processor/internal/utils/json" + "github.com/SAP/remote-work-processor/internal/utils" ) const PASSCODE string = "passcode" @@ -20,20 +19,20 @@ func NewIasAuthorizationHeader(tokenUrl, user, clientCert string) AuthorizationH } } -func (h *iasAuthorizationHeader) Generate() (AuthorizationHeader, error) { +func (h *iasAuthorizationHeader) Generate() (string, error) { raw, err := h.fetcher.Fetch() if err != nil { - return nil, err + return "", fmt.Errorf("failed to fetch IAS token: %v", err) } - parsed := map[string]any{} - if err := json.FromJson(raw, &parsed); err != nil { - return nil, err + parsed := make(map[string]any) + if err = utils.FromJson(raw, &parsed); err != nil { + return "", fmt.Errorf("failed to parse IAS token response: %v", err) } pass, prs := parsed[PASSCODE] if !prs { - return nil, fmt.Errorf("passcode does not exist in the http response") + return "", fmt.Errorf("passcode does not exist in the HTTP response") } return NewBasicAuthorizationHeader(h.user, pass.(string)).Generate() diff --git a/internal/executors/http/ias_token_fetcher.go b/internal/executors/http/ias_token_fetcher.go index 3c6a6e0..d1142bd 100644 --- a/internal/executors/http/ias_token_fetcher.go +++ b/internal/executors/http/ias_token_fetcher.go @@ -4,7 +4,6 @@ import ( "net/http" "github.com/SAP/remote-work-processor/internal/executors/http/tls" - "github.com/SAP/remote-work-processor/internal/functional" ) type iasTokenFetcher struct { @@ -16,7 +15,7 @@ type iasTokenFetcher struct { func NewIasTokenFetcher(tokenUrl, user, clientCert string) TokenFetcher { return &iasTokenFetcher{ - HttpExecutor: DefaultHttpRequestExecutor(), + HttpExecutor: NewDefaultHttpRequestExecutor(), tokenUrl: tokenUrl, user: user, clientCert: clientCert, @@ -24,26 +23,20 @@ func NewIasTokenFetcher(tokenUrl, user, clientCert string) TokenFetcher { } func (f *iasTokenFetcher) Fetch() (string, error) { - p := f.createRequestParameters() + params, _ := f.createRequestParameters() - r, err := f.HttpExecutor.ExecuteWithParameters(p) + resp, err := f.HttpExecutor.ExecuteWithParameters(params) if err != nil { return "", err } - return r.Content, nil + return resp.Content, nil } -func (f *iasTokenFetcher) createRequestParameters() *HttpRequestParameters { - opts := []functional.OptionWithError[HttpRequestParameters]{ - WithUrl(f.tokenUrl), - WithMethod(http.MethodGet), - WithCertificateAuthentication( - tls.NewCertAuthentication( - tls.WithClientCertificate(f.clientCert), - ), +func (f *iasTokenFetcher) createRequestParameters() (*HttpRequestParameters, error) { + return NewHttpRequestParameters(http.MethodGet, f.tokenUrl, WithCertificateAuthentication( + tls.NewCertAuthentication( + tls.WithClientCertificate(f.clientCert), ), - } - - return NewHttpRequestParameters(opts...) + )) } diff --git a/internal/executors/http/oauth_header.go b/internal/executors/http/oauth_header.go deleted file mode 100644 index 326ec0e..0000000 --- a/internal/executors/http/oauth_header.go +++ /dev/null @@ -1,124 +0,0 @@ -package http - -import ( - "fmt" - "log" - "sync" - "time" - - "github.com/SAP/remote-work-processor/internal/executors/http/tls" -) - -const ( - CONTENT_TYPE_HEADER string = "Content-Type" - CONTENT_TYPE_URL_ENCODED string = "application/x-www-form-urlencoded" - TOKEN_EXPIRATION_TIME_PERCENTAGE float32 = 0.95 -) - -type oAuthorizationHeaderOption func(*oAuthorizationHeader) - -type oAuthorizationHeader struct { - tokenType TokenType - grantType GrantType - token *OAuthToken - tokenUrl string - executor HttpExecutor - requestBody string - certAuthentication *tls.CertificateAuthentication - cachingKey string - fetcher TokenFetcher - m *sync.Mutex -} - -func NewOAuthorizationHeader(tokenType TokenType, grantType GrantType, tokenUrl string, executor HttpExecutor, requestBody string, cachingKey string, opts ...oAuthorizationHeaderOption) AuthorizationHeaderGenerator { - h := &oAuthorizationHeader{ - tokenType: tokenType, - grantType: grantType, - token: &OAuthToken{}, - tokenUrl: tokenUrl, - executor: executor, - requestBody: requestBody, - cachingKey: cachingKey, - m: &sync.Mutex{}, - } - - for _, opt := range opts { - opt(h) - } - - h.fetcher = NewOAuthTokenFetcher( - withExecutor(executor), - withTokenUrl(tokenUrl), - withRequestBody(requestBody), - withCertificateAuthentication(h.certAuthentication, func(auth *tls.CertificateAuthentication) bool { return auth != nil }), - ) - - return h -} - -func UseCertificateAuthentication(certAuthentication *tls.CertificateAuthentication) oAuthorizationHeaderOption { - return func(h *oAuthorizationHeader) { - h.certAuthentication = certAuthentication - } -} - -func (h *oAuthorizationHeader) Generate() (AuthorizationHeader, error) { - h.m.Lock() - defer h.m.Unlock() - - if !h.token.HasValue() || h.tokenAboutToExpire() { - if err := h.fetchToken(); err != nil { - return nil, err - } - } - - var token string - switch h.tokenType { - case TokenType_ACCESS: - token = h.token.AccessToken - case TokenType_ID: - token = h.token.IdToken - default: - return nil, NewIllegalTokenTypeError(h.tokenType) - } - - return NewCacheableAuthorizationHeaderView(bearerToken(token), h), nil -} - -func bearerToken(token string) string { - return fmt.Sprintf("Bearer %s", token) -} - -func (h *oAuthorizationHeader) tokenAboutToExpire() bool { - issuedAt := h.token.issuedAt - if issuedAt <= 0.0 { - log.Fatalf("OAuth token is not initialized properly.") - } - - return float32(issuedAt+h.token.ExpiresIn) >= TOKEN_EXPIRATION_TIME_PERCENTAGE*float32(time.Now().UnixMilli()) -} - -func (h *oAuthorizationHeader) setToken(token string, issuedAt int64) error { - t, err := NewOAuthToken(token, issuedAt) - if err != nil { - return err - } - - h.token = t - return nil -} - -func (h *oAuthorizationHeader) fetchToken() error { - token, err := h.fetcher.Fetch() - if err != nil { - return err - } - - issuedAt := time.Now().UnixMilli() - - err = h.setToken(token, issuedAt) - if err != nil { - return err - } - return nil -} diff --git a/internal/executors/http/oauth_header_generator.go b/internal/executors/http/oauth_header_generator.go index c96ea40..2c2d921 100644 --- a/internal/executors/http/oauth_header_generator.go +++ b/internal/executors/http/oauth_header_generator.go @@ -1,179 +1,130 @@ package http import ( - "crypto/sha256" - "encoding/hex" "fmt" - "log" - "net/url" + "github.com/SAP/remote-work-processor/internal/utils" + "time" "github.com/SAP/remote-work-processor/internal/executors/http/tls" ) -const ( - CACHING_KEY_FORMAT string = "tokenUrl=%s&oAuthUser=%s&oAuthPwd=%s&getTokenBody=%s" - PASSWORD_GRANT_FORMAT string = "grant_type=password&username=%s&password=%s" - PASSWORD_CREDENTIALS_FORMAT_WITH_CLIENT_ID string = "grant_type=password&client_id=%s&username=%s&password=%s" - CLIENT_CREDENTIALS_FORMAT string = "grant_type=client_credentials&client_id=%s&client_secret=%s" - REFRESH_TOKEN_FORMAT string = "grant_type=refresh_token&refresh_token=%s" - REFRESH_TOKEN_FORMAT_WITH_CERT string = "grant_type=refresh_token&client_id=%s&refresh_token=%s" -) - -func NewOAuthHeaderGenerator(p *HttpRequestParameters) AuthorizationHeaderGenerator { - user := p.GetUser() - clientId := p.GetClientId() - refreshToken := p.GetRefreshToken() +type OAuthorizationHeaderOption func(*oAuthorizationHeaderGenerator) - if refreshToken != "" { - return refreshTokenGenerator(p) - } +type oAuthorizationHeaderGenerator struct { + tokenType TokenType + certAuthentication *tls.CertificateAuthentication + authHeader string + cachingKey string + requestStore map[string]string + fetcher TokenFetcher +} - if user != "" && clientId != "" { - if p.GetCertificateAuthentication().GetClientCertificate() != "" { - return passwordGrantWithClientCertificateGenerator(p) - } +type cachedToken struct { + *OAuthToken + IssuedAt int64 `json:"timestamp,omitempty"` +} - return passwordGrantGenerator(p) +func NewOAuthorizationHeaderGenerator(tokenType TokenType, tokenUrl string, executor HttpExecutor, requestBody string, + opts ...OAuthorizationHeaderOption) CacheableAuthorizationHeaderGenerator { + h := &oAuthorizationHeaderGenerator{ + tokenType: tokenType, } - if user != "" { - return clientCredentialsGenerator(p, user, p.GetPassword()) + for _, opt := range opts { + opt(h) } - if clientId != "" { - return clientCredentialsGenerator(p, clientId, p.GetClientSecret()) - } + h.fetcher = NewOAuthTokenFetcher( + withExecutor(executor), + withTokenUrl(tokenUrl), + withRequestBody(requestBody), + withCertificateAuthentication(h.certAuthentication), + withAuthHeader(h.authHeader), + ) - return nil + return h } -func passwordGrantGenerator(p *HttpRequestParameters) AuthorizationHeaderGenerator { - tokenUrl := p.GetTokenUrl() - clientId := p.GetClientId() - clientSecret := p.GetClientSecret() - b := fmt.Sprintf(PASSWORD_GRANT_FORMAT, urlEncoded(p.GetUser()), urlEncoded(p.GetPassword())) - - return NewOAuthorizationHeader( - TokenType_ACCESS, - GrantType_PASSWORD, - tokenUrl, - NewHttpRequestExecutor(generateBasicAuthorizationHeader(clientId, clientSecret)), - b, - generateCachingKey(tokenUrl, clientId, clientSecret, b), - ) +func UseCertificateAuthentication(certAuthentication *tls.CertificateAuthentication) OAuthorizationHeaderOption { + return func(h *oAuthorizationHeaderGenerator) { + h.certAuthentication = certAuthentication + } } -func passwordGrantWithClientCertificateGenerator(p *HttpRequestParameters) AuthorizationHeaderGenerator { - tokenUrl := p.GetTokenUrl() - clientId := p.GetClientId() - b := fmt.Sprintf(PASSWORD_CREDENTIALS_FORMAT_WITH_CLIENT_ID, urlEncoded(clientId), urlEncoded(p.GetUser()), urlEncoded(p.GetPassword())) - - return NewOAuthorizationHeader( - TokenType_ACCESS, - GrantType_PASSWORD, - p.GetTokenUrl(), - DefaultHttpRequestExecutor(), - b, - generateCachingKey(tokenUrl, clientId, "", b), - UseCertificateAuthentication(p.certAuthentication), - ) +func WithAuthenticationHeader(header string) OAuthorizationHeaderOption { + return func(h *oAuthorizationHeaderGenerator) { + h.authHeader = header + } } -func clientCredentialsGenerator(p *HttpRequestParameters, clientId string, clientSecret string) AuthorizationHeaderGenerator { - tokenUrl := p.GetTokenUrl() - b := fmt.Sprintf(CLIENT_CREDENTIALS_FORMAT, urlEncoded(clientId), urlEncoded(clientSecret)) - - var h AuthorizationHeader +func WithCachingKey(cacheKey string) OAuthorizationHeaderOption { + return func(h *oAuthorizationHeaderGenerator) { + h.cachingKey = cacheKey + } +} - if clientId != "" && p.certAuthentication.GetClientCertificate() == "" { - h = generateBasicAuthorizationHeader(clientId, clientSecret) +func (h *oAuthorizationHeaderGenerator) Generate() (string, error) { + oAuthToken, err := h.fetchToken() + if err != nil { + return "", err } - return NewOAuthorizationHeader( - TokenType_ACCESS, - GrantType_CLIENT_CREDENTIALS, - tokenUrl, - resolveHttpExecutor(h), - b, - generateCachingKey(tokenUrl, clientId, clientSecret, b), - UseCertificateAuthentication(p.certAuthentication), - ) + return h.formatToken(oAuthToken) } -func refreshTokenGenerator(p *HttpRequestParameters) AuthorizationHeaderGenerator { - tokenUrl := p.GetTokenUrl() - clientId := p.GetClientId() - clientSecret := p.GetClientSecret() - refreshToken := p.GetRefreshToken() - - if p.certAuthentication.GetClientCertificate() == "" { - return refreshTokenGrant(tokenUrl, clientId, clientSecret, refreshToken) - } else { - return refreshTokenGrantWithClientCert(tokenUrl, clientId, refreshToken, p.certAuthentication) +func (h *oAuthorizationHeaderGenerator) GenerateWithCacheAside() (string, error) { + var cached cachedToken + if cachedValue, inCache := h.requestStore[h.cachingKey]; inCache { + if err := utils.FromJson(cachedValue, &cached); err != nil { + return "", fmt.Errorf("failed to deserialize cached OAuth token: %v", err) + } } -} -func refreshTokenGrantWithClientCert(tokenUrl, clientId, refreshToken string, certAuthentication *tls.CertificateAuthentication) AuthorizationHeaderGenerator { - b := fmt.Sprintf(REFRESH_TOKEN_FORMAT_WITH_CERT, urlEncoded(clientId), urlEncoded(refreshToken)) - emptyClientSecret := "" - - return NewOAuthorizationHeader( - TokenType_ACCESS, - GrantType_REFRESH_TOKEN, - tokenUrl, - DefaultHttpRequestExecutor(), - b, - generateCachingKey(tokenUrl, clientId, emptyClientSecret, b), - UseCertificateAuthentication(certAuthentication), - ) -} + if h.tokenAboutToExpire(cached) { + newToken, err := h.fetchToken() + if err != nil { + return "", err + } -func refreshTokenGrant(tokenUrl, clientId, clientSecret, refreshToken string) AuthorizationHeaderGenerator { - b := fmt.Sprintf(REFRESH_TOKEN_FORMAT, urlEncoded(refreshToken)) + cached = cachedToken{ + OAuthToken: newToken, + IssuedAt: time.Now().UnixMilli(), + } - var h AuthorizationHeader + newCachedToken, err := utils.ToJson(cached) + if err != nil { + return "", fmt.Errorf("failed to serialize cached OAuth token: %v", err) + } - if clientId != "" { - h = generateBasicAuthorizationHeader(clientId, clientSecret) + h.requestStore[h.cachingKey] = newCachedToken } - return NewOAuthorizationHeader( - TokenType_ACCESS, - GrantType_REFRESH_TOKEN, - tokenUrl, - resolveHttpExecutor(h), - b, - generateCachingKey(tokenUrl, clientId, clientSecret, b), - ) + return h.formatToken(cached.OAuthToken) } -func generateBasicAuthorizationHeader(clientId string, clientSecret string) AuthorizationHeader { - h, err := NewBasicAuthorizationHeader(clientId, clientSecret).Generate() +func (h *oAuthorizationHeaderGenerator) tokenAboutToExpire(token cachedToken) bool { + // copied from OAuth2BearerAuthorizationHeader.java::isTokenAboutToExpire + return time.Now().Add(30 * time.Second).After(time.UnixMilli(token.IssuedAt + token.ExpiresIn)) +} +func (h *oAuthorizationHeaderGenerator) fetchToken() (*OAuthToken, error) { + rawToken, err := h.fetcher.Fetch() if err != nil { - log.Fatalf("Error occurred while trying to get refresh token: %v\n", err) + return nil, fmt.Errorf("failed to fetch OAuth token: %v", err) } - - return h + return NewOAuthToken(rawToken) } -func resolveHttpExecutor(h AuthorizationHeader) HttpExecutor { - if h != nil { - return NewHttpRequestExecutor(h) - } else { - return DefaultHttpRequestExecutor() +func (h *oAuthorizationHeaderGenerator) formatToken(oAuthToken *OAuthToken) (string, error) { + var token string + switch h.tokenType { + case TokenType_ACCESS: + token = oAuthToken.AccessToken + case TokenType_ID: + token = oAuthToken.IdToken + default: + return "", NewIllegalTokenTypeError(h.tokenType) } -} - -func urlEncoded(query string) string { - return url.QueryEscape(query) -} - -// TODO: TOTP should be considered as part of caching key here as well -func generateCachingKey(tokenUrl string, clientId string, clientSecret string, requestBody string) string { - h := sha256.New() - v := fmt.Sprintf(CACHING_KEY_FORMAT, tokenUrl, clientId, clientSecret, requestBody) - h.Write([]byte(v)) - return hex.EncodeToString(h.Sum(nil)) + return fmt.Sprintf("Bearer %s", token), nil } diff --git a/internal/executors/http/oauth_header_generator_factory.go b/internal/executors/http/oauth_header_generator_factory.go new file mode 100644 index 0000000..3ac402c --- /dev/null +++ b/internal/executors/http/oauth_header_generator_factory.go @@ -0,0 +1,151 @@ +package http + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "net/url" + + "github.com/SAP/remote-work-processor/internal/executors/http/tls" +) + +const ( + CACHING_KEY_FORMAT string = "tokenUrl=%s&oAuthUser=%s&oAuthPwd=%s&getTokenBody=%s" + PASSWORD_GRANT_FORMAT string = "grant_type=password&username=%s&password=%s" + PASSWORD_CREDENTIALS_FORMAT_WITH_CLIENT_ID string = "grant_type=password&client_id=%s&username=%s&password=%s" + CLIENT_CREDENTIALS_FORMAT string = "grant_type=client_credentials&client_id=%s&client_secret=%s" + REFRESH_TOKEN_FORMAT string = "grant_type=refresh_token&refresh_token=%s" + REFRESH_TOKEN_FORMAT_WITH_CERT string = "grant_type=refresh_token&client_id=%s&refresh_token=%s" +) + +func NewOAuthHeaderGenerator(p *HttpRequestParameters) CacheableAuthorizationHeaderGenerator { + user := p.GetUser() + clientId := p.GetClientId() + refreshToken := p.GetRefreshToken() + + if refreshToken != "" { + return refreshTokenGenerator(p) + } + + if user != "" && clientId != "" { + if p.GetCertificateAuthentication().GetClientCertificate() != "" { + return passwordGrantWithClientCertificateGenerator(p) + } + + return passwordGrantGenerator(p) + } + + if user != "" { + return clientCredentialsGenerator(p, user, p.GetPassword()) + } + + if clientId != "" { + return clientCredentialsGenerator(p, clientId, p.GetClientSecret()) + } + + return nil // what happens here? +} + +func passwordGrantGenerator(p *HttpRequestParameters) CacheableAuthorizationHeaderGenerator { + tokenUrl := p.GetTokenUrl() + clientId := p.GetClientId() + clientSecret := p.GetClientSecret() + body := fmt.Sprintf(PASSWORD_GRANT_FORMAT, urlEncoded(p.GetUser()), urlEncoded(p.GetPassword())) + + return NewOAuthorizationHeaderGenerator(TokenType_ACCESS, + tokenUrl, + NewDefaultHttpRequestExecutor(), + body, + WithAuthenticationHeader(generateBasicAuthorizationHeader(clientId, clientSecret)), + WithCachingKey(generateCachingKey(tokenUrl, clientId, clientSecret, body))) +} + +func passwordGrantWithClientCertificateGenerator(p *HttpRequestParameters) CacheableAuthorizationHeaderGenerator { + tokenUrl := p.GetTokenUrl() + clientId := p.GetClientId() + body := fmt.Sprintf(PASSWORD_CREDENTIALS_FORMAT_WITH_CLIENT_ID, urlEncoded(clientId), urlEncoded(p.GetUser()), + urlEncoded(p.GetPassword())) + + return NewOAuthorizationHeaderGenerator(TokenType_ACCESS, + p.GetTokenUrl(), + NewDefaultHttpRequestExecutor(), + body, + UseCertificateAuthentication(p.GetCertificateAuthentication()), + WithCachingKey(generateCachingKey(tokenUrl, clientId, "", body))) +} + +func clientCredentialsGenerator(p *HttpRequestParameters, clientId string, clientSecret string) CacheableAuthorizationHeaderGenerator { + tokenUrl := p.GetTokenUrl() + body := fmt.Sprintf(CLIENT_CREDENTIALS_FORMAT, urlEncoded(clientId), urlEncoded(clientSecret)) + + var opt OAuthorizationHeaderOption + + if clientId != "" && p.GetCertificateAuthentication().GetClientCertificate() == "" { + opt = WithAuthenticationHeader(generateBasicAuthorizationHeader(clientId, clientSecret)) + } else { + opt = UseCertificateAuthentication(p.GetCertificateAuthentication()) + } + + return NewOAuthorizationHeaderGenerator(TokenType_ACCESS, + tokenUrl, + NewDefaultHttpRequestExecutor(), + body, + opt, + WithCachingKey(generateCachingKey(tokenUrl, clientId, clientSecret, body))) +} + +func refreshTokenGenerator(p *HttpRequestParameters) CacheableAuthorizationHeaderGenerator { + tokenUrl := p.GetTokenUrl() + clientId := p.GetClientId() + clientSecret := p.GetClientSecret() + refreshToken := p.GetRefreshToken() + + if p.GetCertificateAuthentication().GetClientCertificate() == "" { + return refreshTokenGrant(tokenUrl, clientId, clientSecret, refreshToken) + } else { + return refreshTokenGrantWithClientCert(tokenUrl, clientId, refreshToken, p.GetCertificateAuthentication()) + } +} + +func refreshTokenGrantWithClientCert(tokenUrl, clientId, refreshToken string, certAuthentication *tls.CertificateAuthentication) CacheableAuthorizationHeaderGenerator { + body := fmt.Sprintf(REFRESH_TOKEN_FORMAT_WITH_CERT, urlEncoded(clientId), urlEncoded(refreshToken)) + + return NewOAuthorizationHeaderGenerator(TokenType_ACCESS, + tokenUrl, + NewDefaultHttpRequestExecutor(), + body, + UseCertificateAuthentication(certAuthentication), + WithCachingKey(generateCachingKey(tokenUrl, clientId, "", body))) +} + +func refreshTokenGrant(tokenUrl, clientId, clientSecret, refreshToken string) CacheableAuthorizationHeaderGenerator { + body := fmt.Sprintf(REFRESH_TOKEN_FORMAT, urlEncoded(refreshToken)) + + var opts []OAuthorizationHeaderOption + if clientId != "" { + opts = append(opts, WithAuthenticationHeader(generateBasicAuthorizationHeader(clientId, clientSecret))) + } + opts = append(opts, WithCachingKey(generateCachingKey(tokenUrl, clientId, clientSecret, body))) + + return NewOAuthorizationHeaderGenerator(TokenType_ACCESS, + tokenUrl, + NewDefaultHttpRequestExecutor(), + body, + opts...) +} + +func generateBasicAuthorizationHeader(clientId string, clientSecret string) string { + header, _ := NewBasicAuthorizationHeader(clientId, clientSecret).Generate() + return header +} + +func urlEncoded(query string) string { + return url.QueryEscape(query) +} + +// TODO: TOTP should be considered as part of caching key here as well +func generateCachingKey(tokenUrl string, clientId string, clientSecret string, requestBody string) string { + h := sha256.New() + h.Write(fmt.Appendf(nil, CACHING_KEY_FORMAT, tokenUrl, clientId, clientSecret, requestBody)) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/internal/executors/http/token.go b/internal/executors/http/oauth_token.go similarity index 58% rename from internal/executors/http/token.go rename to internal/executors/http/oauth_token.go index 0322b6a..7baf1e6 100644 --- a/internal/executors/http/token.go +++ b/internal/executors/http/oauth_token.go @@ -1,25 +1,21 @@ package http -import "encoding/json" +import ( + "encoding/json" + "fmt" +) type OAuthToken struct { TokenType string `json:"token_type"` AccessToken string `json:"access_token"` IdToken string `json:"id_token,omitempty"` ExpiresIn int64 `json:"expires_in,omitempty"` - issuedAt int64 `json:"-"` } -func NewOAuthToken(token string, issuedAt int64) (*OAuthToken, error) { +func NewOAuthToken(token string) (*OAuthToken, error) { oauth := &OAuthToken{} if err := json.Unmarshal([]byte(token), oauth); err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse OAuth token: %v", err) } - - oauth.issuedAt = issuedAt return oauth, nil } - -func (t OAuthToken) HasValue() bool { - return t.AccessToken != "" -} diff --git a/internal/executors/http/oauth_token_fetcher.go b/internal/executors/http/oauth_token_fetcher.go index d773da4..432ee50 100644 --- a/internal/executors/http/oauth_token_fetcher.go +++ b/internal/executors/http/oauth_token_fetcher.go @@ -11,6 +11,7 @@ type oAuthTokenFetcher struct { HttpExecutor tokenUrl string body string + authHeader string certAuthentication *tls.CertificateAuthentication } @@ -42,43 +43,46 @@ func withRequestBody(body string) functional.Option[oAuthTokenFetcher] { } } -func withCertificateAuthentication(auth *tls.CertificateAuthentication, p functional.Predicate[*tls.CertificateAuthentication]) functional.Option[oAuthTokenFetcher] { +func withAuthHeader(header string) functional.Option[oAuthTokenFetcher] { return func(f *oAuthTokenFetcher) { - if p(auth) { - f.certAuthentication = auth - } + f.authHeader = header + } +} + +func withCertificateAuthentication(auth *tls.CertificateAuthentication) functional.Option[oAuthTokenFetcher] { + return func(f *oAuthTokenFetcher) { + f.certAuthentication = auth } } func (f *oAuthTokenFetcher) Fetch() (string, error) { - p := f.createRequestParameters() + params, _ := f.createRequestParameters() // TODO: TOTP should be handled here - r, err := f.HttpExecutor.ExecuteWithParameters(p) + req, err := f.HttpExecutor.ExecuteWithParameters(params) if err != nil { return "", err } - return r.Content, nil + return req.Content, nil } -func (f *oAuthTokenFetcher) createRequestParameters() *HttpRequestParameters { +func (f *oAuthTokenFetcher) createRequestParameters() (*HttpRequestParameters, error) { opts := []functional.OptionWithError[HttpRequestParameters]{ - WithUrl(f.tokenUrl), - WithMethod(http.MethodPost), WithHeaders(ContentTypeUrlFormEncoded()), WithBody(f.body), + WithAuthorizationHeader(f.authHeader), } if f.certAuthentication != nil { opts = append(opts, WithCertificateAuthentication(f.certAuthentication)) } - return NewHttpRequestParameters(opts...) + return NewHttpRequestParameters(http.MethodPost, f.tokenUrl, opts...) } func ContentTypeUrlFormEncoded() map[string]string { return map[string]string{ - CONTENT_TYPE_HEADER: CONTENT_TYPE_URL_ENCODED, + "Content-Type": "application/x-www-form-urlencoded", } } diff --git a/internal/executors/http/tls/tls_configuration_provider.go b/internal/executors/http/tls/tls_configuration_provider.go index 3025a2d..f802fe2 100644 --- a/internal/executors/http/tls/tls_configuration_provider.go +++ b/internal/executors/http/tls/tls_configuration_provider.go @@ -11,53 +11,50 @@ import ( "encoding/pem" "log" "net/http" - "regexp" "github.com/SAP/remote-work-processor/internal/executors" ) const ( - BASE64_ENCODING_PATTERN = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$" PEM_CERTIFICATE_BLOCK_TYPE = "CERTIFICATE" ) -type TLSConfigurationProvider struct { +type ConfigurationProvider struct { *CertificateAuthentication certPool *x509.CertPool } -func NewTLSConfigurationProvider(certAuth *CertificateAuthentication) *TLSConfigurationProvider { - provider := &TLSConfigurationProvider{ +func NewTLSConfigurationProvider(certAuth *CertificateAuthentication) *ConfigurationProvider { + return &ConfigurationProvider{ CertificateAuthentication: certAuth, certPool: ensureCertificatePool(), } - - return provider } -func (p *TLSConfigurationProvider) CreateTransport() (http.RoundTripper, error) { +func (p *ConfigurationProvider) CreateTransport() (http.RoundTripper, error) { t := &http.Transport{ TLSClientConfig: &tls.Config{}, } - if p.TrustAnyCertificate() { - t.TLSClientConfig.InsecureSkipVerify = p.TrustAnyCertificate() - } else if p.UseTrustedCertificates() { - p.trustCertificate(t, p.trustedCerts, "Failed to register the trusted certificate") + t.TLSClientConfig.InsecureSkipVerify = p.TrustAnyCertificate() + + if p.UseTrustedCertificates() { + if err := p.trustCertificate(t, p.trustedCerts, "Failed to register the trusted certificate"); err != nil { + return nil, err + } } if p.UseClientCertificate() { - p.registerClientCertificate(t, p.clientCert, "Failed to register the client certificate and its' private key") + if err := p.registerClientCertificate(t, p.clientCert); err != nil { + return nil, err + } } return t, nil } -func (p *TLSConfigurationProvider) registerClientCertificate(tr *http.Transport, certs string, errMessage string) error { - certs, err := decodeIfBase64Certificate(certs, errMessage) - if err != nil { - return err - } +func (p *ConfigurationProvider) registerClientCertificate(tr *http.Transport, certs string) error { + certs = decodeIfBase64Certificate(certs) cert, err := parseCertificate([]byte(certs)) if err != nil { @@ -73,9 +70,8 @@ func parseCertificate(certs []byte) (tls.Certificate, error) { var err error for { - b, rest := pem.Decode([]byte(certs)) + b, rest := pem.Decode(certs) if b == nil && len(rest) == 0 { - log.Println("All PEM blocks have been read") break } @@ -110,12 +106,8 @@ func parsePK(block []byte) (crypto.PrivateKey, error) { return nil, executors.NewNonRetryableError("Failed to parse client private key") } -func (p *TLSConfigurationProvider) trustCertificate(tr *http.Transport, certs string, errMessage string) error { - certs, err := decodeIfBase64Certificate(certs, errMessage) - if err != nil { - return err - } - +func (p *ConfigurationProvider) trustCertificate(tr *http.Transport, certs string, errMessage string) error { + certs = decodeIfBase64Certificate(certs) ok := p.certPool.AppendCertsFromPEM([]byte(certs)) if !ok { return executors.NewNonRetryableError(errMessage) @@ -125,29 +117,19 @@ func (p *TLSConfigurationProvider) trustCertificate(tr *http.Transport, certs st return nil } -func decodeIfBase64Certificate(certs string, errMessage string) (string, error) { - if !isBase64(certs) { - return certs, nil - } - - d, err := base64.StdEncoding.DecodeString(certs) +func decodeIfBase64Certificate(certs string) string { + decoded, err := base64.StdEncoding.DecodeString(certs) if err != nil { - return "", executors.NewNonRetryableError(errMessage) + return certs } - - return string(d), nil -} - -func isBase64(s string) bool { - return regexp.MustCompile(BASE64_ENCODING_PATTERN).MatchString(s) + return string(decoded) } func ensureCertificatePool() *x509.CertPool { pool, err := x509.SystemCertPool() if err != nil { - log.Printf("Failed to get system certificate pool, a new one will be created.") + log.Println("Failed to get system certificate pool, a new one will be created.") pool = x509.NewCertPool() } - return pool } diff --git a/internal/executors/http/token_fetcher.go b/internal/executors/http/token_fetcher.go index ec5e401..5fcd48a 100644 --- a/internal/executors/http/token_fetcher.go +++ b/internal/executors/http/token_fetcher.go @@ -2,5 +2,5 @@ package http type TokenFetcher interface { Fetch() (string, error) - createRequestParameters() *HttpRequestParameters + createRequestParameters() (*HttpRequestParameters, error) } diff --git a/internal/executors/http/token_type.go b/internal/executors/http/token_type.go index 73d8d5d..3d439a7 100644 --- a/internal/executors/http/token_type.go +++ b/internal/executors/http/token_type.go @@ -14,7 +14,3 @@ var ( func (t TokenType) String() string { return tokenTypeNames[t] } - -func (e TokenType) Ordinal() uint { - return uint(e) -} diff --git a/internal/executors/void/void_executor.go b/internal/executors/void/void_executor.go index 265fed2..c264cfb 100644 --- a/internal/executors/void/void_executor.go +++ b/internal/executors/void/void_executor.go @@ -3,17 +3,17 @@ package void import ( pb "github.com/SAP/remote-work-processor/build/proto/generated" "github.com/SAP/remote-work-processor/internal/executors" + "log" ) const ( MESSAGE_KEY = "message" ) -type VoidExecutor struct { - executors.Executor -} +type VoidExecutor struct{} -func (e *VoidExecutor) Execute(ctx executors.ExecutorContext) *executors.ExecutorResult { +func (VoidExecutor) Execute(ctx executors.Context) *executors.ExecutorResult { + log.Println("Executing Void command...") msg := ctx.GetString(MESSAGE_KEY) return executors.NewExecutorResult( executors.Output(buildOutput(msg)), @@ -21,8 +21,8 @@ func (e *VoidExecutor) Execute(ctx executors.ExecutorContext) *executors.Executo ) } -func buildOutput(msg string) map[string]interface{} { - return map[string]interface{}{ +func buildOutput(msg string) map[string]string { + return map[string]string{ MESSAGE_KEY: msg, } } diff --git a/internal/functional/types.go b/internal/functional/types.go deleted file mode 100644 index 0f4447c..0000000 --- a/internal/functional/types.go +++ /dev/null @@ -1,13 +0,0 @@ -package functional - -type Consumer[T any] func(t T) -type Predicate[T any] func(t T) bool -type Supplier[T any] func() T -type Function[T any, R any] func(t T) R -type UnaryOperator[T any] Function[T, T] - -type BiConsumer[T any, U any] func(t T, u U) -type BiPredicate[T any, U any] func(t T, u U) -type BiSupplier[T any, U any] func() (T, U) -type BiFunction[T any, U any, R any] func(t T, u U) R -type BinaryOperator[T any] BiFunction[T, T, T] diff --git a/internal/grpc/client.go b/internal/grpc/client.go index 77e5886..5a3da25 100644 --- a/internal/grpc/client.go +++ b/internal/grpc/client.go @@ -5,123 +5,139 @@ import ( "fmt" "io" "log" - "os" "sync" + "time" pb "github.com/SAP/remote-work-processor/build/proto/generated" - "github.com/SAP/remote-work-processor/internal/grpc/processors" meta "github.com/SAP/remote-work-processor/internal/kubernetes/metadata" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" -) - -var ( - HOST string = os.Getenv("AUTOPI_HOSTNAME") - PORT string = os.Getenv("AUTOPI_PORT") -) - -var ( - once sync.Once - Client RemoteWorkProcessorGrpcClient + "google.golang.org/grpc/status" ) type RemoteWorkProcessorGrpcClient struct { sync.Mutex - metadata *GrpcClientMetadata - connection *grpc.ClientConn - context context.Context - cancel context.CancelFunc - grpcClient pb.RemoteWorkProcessorServiceClient - stream pb.RemoteWorkProcessorService_SessionClient + metadata *ClientMetadata + stream pb.RemoteWorkProcessorService_SessionClient + context context.Context + cancelCtx context.CancelFunc +} + +func NewClient(metadata meta.RemoteWorkProcessorMetadata, isStandaloneMode bool) *RemoteWorkProcessorGrpcClient { + return &RemoteWorkProcessorGrpcClient{ + metadata: NewClientMetadata(metadata.AutoPiHost(), metadata.AutoPiPort(), isStandaloneMode). + WithClientCertificate(). + WithBinaryVersion(metadata.BinaryVersion()), + } } -func newClient(host string, port string) RemoteWorkProcessorGrpcClient { - ctx, cf := context.WithCancel(context.Background()) +func (gc *RemoteWorkProcessorGrpcClient) InitSession(baseCtx context.Context, sessionID string) error { + select { + case <-baseCtx.Done(): + return nil + default: + } + + ctx, cancel := context.WithCancel(baseCtx) ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{ - "X-AutoPilot-SessionId": meta.Metadata.Id(), - "X-AutoPilot-BinaryVersion": meta.Metadata.BinaryVersion(), + "X-AutoPilot-SessionId": sessionID, + "X-AutoPilot-BinaryVersion": gc.metadata.GetBinaryVersion(), })) + gc.context = ctx + gc.cancelCtx = cancel - return RemoteWorkProcessorGrpcClient{ - metadata: NewGrpcClientMetadata(host, port).WithClientCertificate().BlockWhenDialing(), - context: ctx, - cancel: cf, + rpc, err := gc.establishConnection(ctx) + if err != nil { + return err } + return gc.startSession(rpc, ctx) } -func InitRemoteWorkProcessorGrpcClient() { - once.Do(func() { - Client = newClient(HOST, PORT) - Client.connect() - Client.openSession() - }) -} +func (gc *RemoteWorkProcessorGrpcClient) Send(op *pb.ClientMessage) error { + select { + case <-gc.context.Done(): + gc.closeConn() + return nil + default: + } -func (gc *RemoteWorkProcessorGrpcClient) Send(op *pb.ClientMessage) { gc.Lock() defer gc.Unlock() if err := gc.stream.Send(op); err != nil { - log.Fatalf("Error occured while sending client message: %v\n", err) - gc.stream.CloseSend() + gc.closeConn() + return fmt.Errorf("error occured while sending client message: %v", err) } + return nil } -func (gc *RemoteWorkProcessorGrpcClient) Receive() <-chan *pb.ServerMessage { - opChan := make(chan *pb.ServerMessage) - go func(c chan *pb.ServerMessage) { - log.Println("Waiting to receive protocol message...") - for { - m, recvErr := gc.stream.Recv() - if recvErr == io.EOF { - log.Print("Server closed the connection. Bye!") - gc.stream.CloseSend() - break - } - - if recvErr != nil { - log.Fatalf("Error occured while receiving message from server: %v\n", recvErr) - } +func (gc *RemoteWorkProcessorGrpcClient) ReceiveMsg() (*pb.ServerMessage, error) { + log.Println("Waiting for server message...") + msg, err := gc.stream.Recv() + if err == io.EOF { + log.Println("Server closed the connection.") + gc.closeConn() + return nil, nil + } - c <- m + if err != nil { + rpcErr, isRpcErr := status.FromError(err) + if isRpcErr && rpcErr.Code() == codes.Canceled { + // context was cancelled + return nil, nil } - }(opChan) - - return opChan + return nil, fmt.Errorf("error occured while receiving message from server: %v", err) + } + return msg, nil } -func (gc *RemoteWorkProcessorGrpcClient) connect() { - connection, err := grpc.Dial(fmt.Sprintf("%s:%s", gc.metadata.host, gc.metadata.port), gc.metadata.options...) +func (gc *RemoteWorkProcessorGrpcClient) establishConnection(ctx context.Context) (pb.RemoteWorkProcessorServiceClient, error) { + target := fmt.Sprintf("%s:%s", gc.metadata.GetHost(), gc.metadata.GetPort()) + log.Println("Connecting to AutoPi at", target) + conn, err := grpc.DialContext(ctx, target, gc.metadata.GetOptions()...) if err != nil { - log.Fatalf("Couldn't connect to gRPC server serving at port %s: %v\n", PORT, err) + return nil, fmt.Errorf("could not connect to gRPC server: %v", err) } - - gc.connection = connection - gc.grpcClient = pb.NewRemoteWorkProcessorServiceClient(connection) + return pb.NewRemoteWorkProcessorServiceClient(conn), nil } -func (gc *RemoteWorkProcessorGrpcClient) openSession() { - if gc.grpcClient == nil { - log.Fatalln("Connection to the gRPC server failed and client has no been initialized. Failed to open session") - } - - stream, err := gc.grpcClient.Session(gc.context) +func (gc *RemoteWorkProcessorGrpcClient) startSession(rpcClient pb.RemoteWorkProcessorServiceClient, ctx context.Context) error { + log.Println("Creating RPC stream session...") + stream, err := rpcClient.Session(ctx) if err != nil { - log.Fatalf("Could not fetch resources watch config from the server: %v\n", err) + return fmt.Errorf("could not start a session with the server: %v", err) } gc.stream = stream + go gc.runHeartbeat() + return nil +} - go func() { - p := processors.Factory.CreateProbeSessionProcessor() - for { - res := <-p.Process() - if res.Err != nil { - log.Fatalf("Error occured while sending heartbeat to backend: %v\n", res.Err) - close(res.Done) +func (gc *RemoteWorkProcessorGrpcClient) runHeartbeat() { + t := time.NewTicker(30 * time.Second) + defer t.Stop() + +Loop: + for { + select { + case <-t.C: + msg := &pb.ClientMessage{ + Body: &pb.ClientMessage_ProbeSession{ + ProbeSession: &pb.ProbeSessionMessage{}, + }, } - - gc.Send(res.Result) + if err := gc.Send(msg); err != nil { + log.Printf("Error sending heartbeat: %v\n", err) + break Loop + } + case <-gc.context.Done(): + break Loop } - }() + } +} + +func (gc *RemoteWorkProcessorGrpcClient) closeConn() { + gc.stream.CloseSend() + gc.cancelCtx() } diff --git a/internal/grpc/client_metadata.go b/internal/grpc/client_metadata.go index 0d93e3c..8f568b6 100644 --- a/internal/grpc/client_metadata.go +++ b/internal/grpc/client_metadata.go @@ -2,6 +2,8 @@ package grpc import ( "crypto/tls" + "encoding/base64" + "github.com/SAP/remote-work-processor/internal/utils" "log" "google.golang.org/grpc" @@ -14,36 +16,77 @@ const ( PRIVATE_KEY = "pk" ) -type GrpcClientMetadata struct { - host string - port string - options []grpc.DialOption +type ClientMetadata struct { + host string + port string + binaryVersion string + options []grpc.DialOption + standaloneMode bool } -func NewGrpcClientMetadata(host string, port string) *GrpcClientMetadata { - return &GrpcClientMetadata{ - host: host, - port: port, - options: make([]grpc.DialOption, 0), +func NewClientMetadata(host string, port string, isStandaloneMode bool) *ClientMetadata { + return &ClientMetadata{ + host: host, + port: port, + standaloneMode: isStandaloneMode, } } -func (gm *GrpcClientMetadata) WithClientCertificate() *GrpcClientMetadata { - clientCert, err := tls.LoadX509KeyPair(CERTIFICATE_MOUTH_PATH+CERTIFICATE_KEY, CERTIFICATE_MOUTH_PATH+PRIVATE_KEY) - if err != nil { - log.Fatalf("could not load client cert: %v", err) - } - +func (cm *ClientMetadata) WithClientCertificate() *ClientMetadata { + cert := cm.getClientCert() config := &tls.Config{ - Certificates: []tls.Certificate{clientCert}, - InsecureSkipVerify: false, + Certificates: []tls.Certificate{cert}, } + cm.options = append(cm.options, grpc.WithTransportCredentials(credentials.NewTLS(config))) + return cm +} + +func (cm *ClientMetadata) WithBinaryVersion(version string) *ClientMetadata { + cm.binaryVersion = version + return cm +} + +func (cm *ClientMetadata) GetHost() string { + return cm.host +} - gm.options = append(gm.options, grpc.WithTransportCredentials(credentials.NewTLS(config))) - return gm +func (cm *ClientMetadata) GetPort() string { + return cm.port } -func (gm *GrpcClientMetadata) BlockWhenDialing() *GrpcClientMetadata { - gm.options = append(gm.options, grpc.WithBlock()) - return gm +func (cm *ClientMetadata) GetBinaryVersion() string { + return cm.binaryVersion +} + +func (cm *ClientMetadata) GetOptions() []grpc.DialOption { + return cm.options +} + +func (cm *ClientMetadata) getClientCert() tls.Certificate { + if cm.standaloneMode { + certChain := utils.GetRequiredEnv("RWP_CERT_CHAIN") + privateKey := utils.GetRequiredEnv("RWP_PRIVATE_KEY") + + certChainBytes, err := base64.StdEncoding.DecodeString(certChain) + if err != nil { + log.Fatalln("Could not decode certificate chain from environment:", err) + } + + privateKeyBytes, err := base64.StdEncoding.DecodeString(privateKey) + if err != nil { + log.Fatalln("Could not decode private key from environment:", err) + } + + cert, err := tls.X509KeyPair(certChainBytes, privateKeyBytes) + if err != nil { + log.Fatalln("Could not load client certificate from environment:", err) + } + return cert + } else { + cert, err := tls.LoadX509KeyPair(CERTIFICATE_MOUTH_PATH+CERTIFICATE_KEY, CERTIFICATE_MOUTH_PATH+PRIVATE_KEY) + if err != nil { + log.Fatalln("Could not load client certificate from files:", err) + } + return cert + } } diff --git a/internal/grpc/processors/disable_processor.go b/internal/grpc/processors/disable_processor.go index 442e02d..4073ad4 100644 --- a/internal/grpc/processors/disable_processor.go +++ b/internal/grpc/processors/disable_processor.go @@ -1,31 +1,30 @@ package processors import ( - "fmt" + "context" + "log" pb "github.com/SAP/remote-work-processor/build/proto/generated" ) -// Currently remote work processor ID should be in the following format - :: type DisableProcessor struct { + disableFunc func() } -func NewDisableProcessor() DisableProcessor { - return DisableProcessor{} +func NewDisableProcessor(disableFunc func()) DisableProcessor { + return DisableProcessor{ + disableFunc: disableFunc, + } } -func (p DisableProcessor) Process() <-chan *ProcessorResult { - c := make(chan *ProcessorResult) - go p.buildClientMessage(c) +func (p DisableProcessor) Process(_ context.Context) (*pb.ClientMessage, error) { + log.Println("Disabling work processor...") - return c -} + p.disableFunc() -func (p DisableProcessor) buildClientMessage(c chan<- *ProcessorResult) { - fmt.Println("DISABLE OPERATOR...") - c <- NewProcessorResult(Result(&pb.ClientMessage{ + return &pb.ClientMessage{ Body: &pb.ClientMessage_ConfirmDisabled{ ConfirmDisabled: &pb.ConfirmDisabledMessage{}, }, - })) + }, nil } diff --git a/internal/grpc/processors/enable_processor.go b/internal/grpc/processors/enable_processor.go index 5a10350..7cbf03d 100644 --- a/internal/grpc/processors/enable_processor.go +++ b/internal/grpc/processors/enable_processor.go @@ -1,31 +1,30 @@ package processors import ( - "fmt" + "context" + "log" pb "github.com/SAP/remote-work-processor/build/proto/generated" ) -// Currently remote work processor ID should be in the following format - :: type EnableProcessor struct { + enableFunc func() } -func NewEnableProcessor() EnableProcessor { - return EnableProcessor{} +func NewEnableProcessor(enableFunc func()) EnableProcessor { + return EnableProcessor{ + enableFunc: enableFunc, + } } -func (p EnableProcessor) Process() <-chan *ProcessorResult { - c := make(chan *ProcessorResult) - go p.buildClientMessage(c) +func (p EnableProcessor) Process(_ context.Context) (*pb.ClientMessage, error) { + log.Println("Enabling work processor...") - return c -} + p.enableFunc() -func (p EnableProcessor) buildClientMessage(c chan<- *ProcessorResult) { - fmt.Println("ENABLING OPERATOR...") - c <- NewProcessorResult(Result(&pb.ClientMessage{ + return &pb.ClientMessage{ Body: &pb.ClientMessage_ConfirmEnabled{ ConfirmEnabled: &pb.ConfirmEnabledMessage{}, }, - })) + }, nil } diff --git a/internal/grpc/processors/errors.go b/internal/grpc/processors/errors.go deleted file mode 100644 index d93a000..0000000 --- a/internal/grpc/processors/errors.go +++ /dev/null @@ -1,21 +0,0 @@ -package processors - -import "fmt" - -type ProcessorError struct { - message string -} - -func NewProcessorError(message string) *ProcessorError { - return &ProcessorError{ - message: message, - } -} - -func (e ProcessorError) Error() string { - if e.message == "" { - return fmt.Sprint(e.message) - } - - return fmt.Sprintf("An error occurred while processing operation") -} diff --git a/internal/grpc/processors/probe_session_processor.go b/internal/grpc/processors/probe_session_processor.go deleted file mode 100644 index 1744b92..0000000 --- a/internal/grpc/processors/probe_session_processor.go +++ /dev/null @@ -1,41 +0,0 @@ -package processors - -import ( - "time" - - pb "github.com/SAP/remote-work-processor/build/proto/generated" -) - -type ProbeSessionProcessor struct { -} - -func NewProbeSessionProcessor() ProbeSessionProcessor { - return ProbeSessionProcessor{} -} - -func (p ProbeSessionProcessor) Process() <-chan *ProcessorResult { - c := make(chan *ProcessorResult) - done := make(chan struct{}) - go p.buildProbeSession(c, done) - - return c -} - -func (p ProbeSessionProcessor) buildProbeSession(c chan<- *ProcessorResult, done chan struct{}) { - t := time.NewTicker(30 * time.Second) - - for { - select { - case <-t.C: - op := &pb.ClientMessage{ - Body: &pb.ClientMessage_ProbeSession{ - ProbeSession: &pb.ProbeSessionMessage{}, - }, - } - - c <- NewProcessorResult(Result(op), OnChannel(done)) - case <-done: - t.Stop() - } - } -} diff --git a/internal/grpc/processors/processor.go b/internal/grpc/processors/processor.go index 1d7ae02..61ed663 100644 --- a/internal/grpc/processors/processor.go +++ b/internal/grpc/processors/processor.go @@ -1,44 +1,11 @@ package processors import ( + "context" + pb "github.com/SAP/remote-work-processor/build/proto/generated" ) type Processor interface { - Process() <-chan *ProcessorResult -} -type ProcessorResult struct { - Result *pb.ClientMessage - Err error - Done chan struct{} -} - -type processorResultOption func(*ProcessorResult) - -func NewProcessorResult(opts ...processorResultOption) *ProcessorResult { - pr := &ProcessorResult{} - - for _, opt := range opts { - opt(pr) - } - - return pr -} - -func Result(r *pb.ClientMessage) processorResultOption { - return func(pr *ProcessorResult) { - pr.Result = r - } -} - -func Error(err error) processorResultOption { - return func(pr *ProcessorResult) { - pr.Err = err - } -} - -func OnChannel(done chan struct{}) processorResultOption { - return func(pr *ProcessorResult) { - pr.Done = done - } + Process(ctx context.Context) (*pb.ClientMessage, error) } diff --git a/internal/grpc/processors/processor_factory.go b/internal/grpc/processors/processor_factory.go index 0b6ef53..dba0900 100644 --- a/internal/grpc/processors/processor_factory.go +++ b/internal/grpc/processors/processor_factory.go @@ -1,44 +1,57 @@ package processors import ( - "sync" - + "fmt" pb "github.com/SAP/remote-work-processor/build/proto/generated" "github.com/SAP/remote-work-processor/internal/kubernetes/engine" -) - -var ( - once sync.Once - Factory ProcessorFactory + "sync/atomic" ) type ProcessorFactory struct { - engine engine.ManagerEngine + engine engine.ManagerEngine + drainChan chan struct{} + rwpEnabled *atomic.Bool } -func InitProcessorFactory(engine engine.ManagerEngine) { - once.Do(func() { - Factory = ProcessorFactory{ - engine: engine, - } - }) +func NewKubernetesProcessorFactory(engine engine.ManagerEngine, drainChan chan struct{}) ProcessorFactory { + enabled := &atomic.Bool{} + enabled.Store(true) + // ensure the channel does not deadlock main() in case no watch config is ever set + drainChan <- struct{}{} + return ProcessorFactory{ + engine: engine, + drainChan: drainChan, + rwpEnabled: enabled, + } +} + +func NewStandaloneProcessorFactory() ProcessorFactory { + enabled := &atomic.Bool{} + enabled.Store(true) + return ProcessorFactory{ + rwpEnabled: enabled, + } } func (pf *ProcessorFactory) CreateProcessor(op *pb.ServerMessage) (Processor, error) { + // TODO: The NextEventRequestMessage message changes the current k8s reconciliation flow. + // Instead of sending an event message to the server on every reconcile loop, + // push these events to a queue (in a separate goroutine). + // That routine will listen for the NextEventRequestMessage and only send messages when it receives it. + // This queue will send reconcilliation event messages to the backend when either: + // - the queue is empty; + // - the queue has elements and there is a NextEventRequestMessage. + // Since this logic hasn't been implemented in the backend yet, it's not present here either. switch b := op.Body.(type) { case *pb.ServerMessage_TaskExecutionRequest: - return NewRemoteTaskProcessor(b), nil + return NewRemoteTaskProcessor(b, pf.rwpEnabled.Load), nil case *pb.ServerMessage_UpdateConfigRequest: - return NewUpdateWatchConfigurationProcessor(op, pf.engine), nil + return NewUpdateWatchConfigurationProcessor(b, pf.engine, pf.drainChan, pf.rwpEnabled.Load), nil case *pb.ServerMessage_DisableRequest: - return NewDisableProcessor(), nil + return NewDisableProcessor(func() { pf.rwpEnabled.Store(false) }), nil case *pb.ServerMessage_EnableRequest: - return NewEnableProcessor(), nil + return NewEnableProcessor(func() { pf.rwpEnabled.Store(true) }), nil default: - return nil, ProcessorError{} + return nil, fmt.Errorf("unrecognized request type %+v", op.Body) } } - -func (pf *ProcessorFactory) CreateProbeSessionProcessor() Processor { - return NewProbeSessionProcessor() -} diff --git a/internal/grpc/processors/remote_task_processor.go b/internal/grpc/processors/remote_task_processor.go index a7a2ef5..4293142 100644 --- a/internal/grpc/processors/remote_task_processor.go +++ b/internal/grpc/processors/remote_task_processor.go @@ -1,7 +1,7 @@ package processors import ( - "encoding/json" + "context" "log" pb "github.com/SAP/remote-work-processor/build/proto/generated" @@ -11,65 +11,51 @@ import ( ) type RemoteTaskProcessor struct { - req *pb.ServerMessage_TaskExecutionRequest + req *pb.TaskExecutionRequestMessage + isEnabled func() bool } -func NewRemoteTaskProcessor(req *pb.ServerMessage_TaskExecutionRequest) RemoteTaskProcessor { +func NewRemoteTaskProcessor(req *pb.ServerMessage_TaskExecutionRequest, isEnabled func() bool) RemoteTaskProcessor { return RemoteTaskProcessor{ - req: req, + req: req.TaskExecutionRequest, + isEnabled: isEnabled, } } -func (p RemoteTaskProcessor) Process() <-chan *ProcessorResult { - c := make(chan *ProcessorResult) - go func() { - log.Println("Processing remote task...") - executor, err := factory.Executor_Factory.GetExecutor(p.req.TaskExecutionRequest.GetType()) - if err != nil { - c <- NewProcessorResult(Error(err)) - } +func (p RemoteTaskProcessor) Process(_ context.Context) (*pb.ClientMessage, error) { + if !p.isEnabled() { + log.Println("Unable to process remote task. Remote Worker is disabled...") + return nil, nil + } - ctx := executors.NewExecutorContext(p.req.TaskExecutionRequest.GetInput(), p.req.TaskExecutionRequest.Store) + log.Println("Processing Task...") + executor, err := factory.CreateExecutor(p.req.GetType()) + if err != nil { + log.Println(err) + // Do not fail and recreate gRPC connection on unsupported task type + return nil, nil + } - res := executor.Execute(ctx) - c <- NewProcessorResult(Result(&pb.ClientMessage{ - Body: buildResult(ctx, p.req, res), - })) - }() + ctx := executors.NewExecutorContext(p.req.GetInput(), p.req.Store) - return c + res := executor.Execute(ctx) + return &pb.ClientMessage{ + Body: buildResult(ctx, p.req, res), + }, nil } -func buildResult(ctx executors.ExecutorContext, req *pb.ServerMessage_TaskExecutionRequest, res *executors.ExecutorResult) *pb.ClientMessage_TaskExecutionResponse { +func buildResult(ctx executors.Context, req *pb.TaskExecutionRequestMessage, res *executors.ExecutorResult) *pb.ClientMessage_TaskExecutionResponse { return &pb.ClientMessage_TaskExecutionResponse{ TaskExecutionResponse: &pb.TaskExecutionResponseMessage{ - ExecutionId: req.TaskExecutionRequest.GetExecutionId(), - ExecutionVersion: req.TaskExecutionRequest.GetExecutionVersion(), + ExecutionId: req.GetExecutionId(), + ExecutionVersion: req.GetExecutionVersion(), State: res.Status, - Output: toStringValues(res.Output), - Store: ctx.GetStore().ToMap(), + Output: res.Output, + Store: ctx.GetStore(), Error: &wrapperspb.StringValue{ Value: res.Error, }, - Type: req.TaskExecutionRequest.Type, + Type: req.Type, }, } } - -func toStringValues(m map[string]interface{}) map[string]string { - out := make(map[string]string) - for k, v := range m { - if str, ok := v.(string); ok { - out[k] = str - continue - } - - b, err := json.Marshal(v) - if err != nil { - log.Fatalf("Failed to serialize value %s: %v", v, err) - } - out[k] = string(b) - } - - return out -} diff --git a/internal/grpc/processors/update_configuration_processor.go b/internal/grpc/processors/update_configuration_processor.go index 436af35..2e621d1 100644 --- a/internal/grpc/processors/update_configuration_processor.go +++ b/internal/grpc/processors/update_configuration_processor.go @@ -1,6 +1,7 @@ package processors import ( + "context" "log" pb "github.com/SAP/remote-work-processor/build/proto/generated" @@ -8,56 +9,67 @@ import ( ) type UpdateWatchConfigurationProcessor struct { - op *pb.ServerMessage - engine engine.ManagerEngine - wcc chan *pb.UpdateConfigRequestMessage + op *pb.ServerMessage_UpdateConfigRequest + engine engine.ManagerEngine + drainChan chan struct{} + isEnabled func() bool } -func NewUpdateWatchConfigurationProcessor(op *pb.ServerMessage, engine engine.ManagerEngine) UpdateWatchConfigurationProcessor { +func NewUpdateWatchConfigurationProcessor(op *pb.ServerMessage_UpdateConfigRequest, engine engine.ManagerEngine, + drainChan chan struct{}, isEnabled func() bool) UpdateWatchConfigurationProcessor { return UpdateWatchConfigurationProcessor{ - op: op, - engine: engine, - wcc: make(chan *pb.UpdateConfigRequestMessage), + op: op, + engine: engine, + drainChan: drainChan, + isEnabled: isEnabled, } } -func (p UpdateWatchConfigurationProcessor) Process() <-chan *ProcessorResult { - c := make(chan *ProcessorResult) - - go func() { - if p.engine.ManagerStartedAtLeastOnce() { - log.Print("Stopping Manager....") - p.engine.StopManager() - } +func (p UpdateWatchConfigurationProcessor) Process(ctx context.Context) (*pb.ClientMessage, error) { + if !p.isEnabled() { + log.Println("Unable to process watch config: Remote Worker is disabled.") + return nil, nil + } - go func() { - for { - wc := <-p.wcc - log.Print("New watch config received. Starting manager....") + if len(p.op.UpdateConfigRequest.Resources) == 0 { + // handle session auto-config + return &pb.ClientMessage{Body: p.getConfirmUpdateMessage()}, nil + } - p.engine.WithWatchConfiguration(wc) - p.engine.WithContext() + if p.engine == nil { + log.Println("Unable to process watch config: Remote Worker is running in standalone mode.") + return nil, nil + } - if err := p.engine.StartManager(); err != nil { - log.Fatalf("unable to start manager: %v\n", err) - } - } - }() + if p.engine.IsRunning() { + log.Println("Stopping Manager...") + p.engine.Stop() + <-p.drainChan + } - uc, ok := p.op.Body.(*pb.ServerMessage_UpdateConfigRequest) - if !ok { - c <- NewProcessorResult(Error(ProcessorError{})) + go func() { + select { + case <-p.drainChan: + //drain in case the manager hasn't been started yet (the processor factory signals this channel) + default: } - p.wcc <- uc.UpdateConfigRequest - c <- NewProcessorResult(Result(&pb.ClientMessage{ - Body: &pb.ClientMessage_ConfirmConfigUpdate{ - ConfirmConfigUpdate: &pb.ConfirmConfigUpdateMessage{ - ConfigVersion: uc.UpdateConfigRequest.GetConfigVersion(), - }, - }, - })) + log.Println("New watch config received...") + p.engine.SetWatchConfiguration(p.op.UpdateConfigRequest) + + if err := p.engine.WatchResources(ctx, p.isEnabled); err != nil { + log.Fatalln("failed to watch resources:", err) + } + p.drainChan <- struct{}{} }() - return c + return &pb.ClientMessage{Body: p.getConfirmUpdateMessage()}, nil +} + +func (p UpdateWatchConfigurationProcessor) getConfirmUpdateMessage() *pb.ClientMessage_ConfirmConfigUpdate { + return &pb.ClientMessage_ConfirmConfigUpdate{ + ConfirmConfigUpdate: &pb.ConfirmConfigUpdateMessage{ + ConfigVersion: p.op.UpdateConfigRequest.GetConfigVersion(), + }, + } } diff --git a/internal/kubernetes/controller/controller.go b/internal/kubernetes/controller/controller.go deleted file mode 100644 index 41285fe..0000000 --- a/internal/kubernetes/controller/controller.go +++ /dev/null @@ -1,100 +0,0 @@ -package controller - -import ( - "context" - "log" - - pb "github.com/SAP/remote-work-processor/build/proto/generated" - "github.com/SAP/remote-work-processor/internal/kubernetes/selector" - "github.com/pkg/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime/schema" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/predicate" -) - -type ResourceControllerBuilder interface { - For(r *pb.Resource) *ControllerBuilder - ManagedBy(m *ControllerManager) *ControllerBuilder - Build() Controller -} - -type Controller struct { - resource *pb.Resource - manager *ControllerManager - reconciliationPeriodInMinutes int32 -} - -type ControllerBuilder struct { - Controller -} - -func CreateControllerBuilder() *ControllerBuilder { - return &ControllerBuilder{} -} - -func (cb *ControllerBuilder) For(r *pb.Resource) *ControllerBuilder { - cb.resource = r - return cb -} - -func (cb *ControllerBuilder) WithReconcilicationPeriodInMinutes(p int32) *ControllerBuilder { - cb.reconciliationPeriodInMinutes = p - return cb -} - -func (cb *ControllerBuilder) ManagedBy(m *ControllerManager) *ControllerBuilder { - cb.manager = m - return cb -} - -func (cb *ControllerBuilder) Build(ctx context.Context, reconciler string) (Controller, error) { - b := ctrl.NewControllerManagedBy(cb.manager.manager) - u := &unstructured.Unstructured{} - gvk := schema.FromAPIVersionAndKind(cb.resource.ApiVersion, cb.resource.Kind) - mapper, err := cb.manager.dynamicClient.GetGVR(&gvk) - if err != nil { - log.Fatalf("Failed to resolve resource type from kind: %v\n", err) - } - - u.SetGroupVersionKind(gvk) - - s := cb.manager.selectorCache.Read(reconciler) - - b.For(u).WithEventFilter(shouldWatchResource(gvk, cb.resource.GetNamespace().GetValue(), &s)) - - err = b.Complete(createReconciler(cb.manager.GetScheme(), cb.manager.dynamicClient, mapper, reconciler, cb.reconciliationPeriodInMinutes)) - if err != nil { - return Controller{}, errors.Errorf("Unable to create a controller: %s", err) - } - - return cb.Controller, nil -} - -func shouldWatchResource(gvk schema.GroupVersionKind, ns string, s *selector.Selector) predicate.Predicate { - return predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { - return isWatchedResource(e.Object, gvk, ns, s) - }, - UpdateFunc: func(e event.UpdateEvent) bool { - return isWatchedResource(e.ObjectNew, gvk, ns, s) - }, - DeleteFunc: func(e event.DeleteEvent) bool { - return isWatchedResource(e.Object, gvk, ns, s) - }, - } -} - -func isWatchedResource(o client.Object, gvk schema.GroupVersionKind, ns string, s *selector.Selector) bool { - var l labels.Set - l = o.GetLabels() - - return o != nil && - o.GetObjectKind().GroupVersionKind() == gvk && - o.GetNamespace() == ns && - s.LabelSelector.Matches(l) && - s.FieldSelector.Matches(o) -} diff --git a/internal/kubernetes/controller/controller_builder.go b/internal/kubernetes/controller/controller_builder.go new file mode 100644 index 0000000..f1121eb --- /dev/null +++ b/internal/kubernetes/controller/controller_builder.go @@ -0,0 +1,91 @@ +package controller + +import ( + "fmt" + pb "github.com/SAP/remote-work-processor/build/proto/generated" + "github.com/SAP/remote-work-processor/internal/grpc" + "github.com/SAP/remote-work-processor/internal/kubernetes/selector" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +type ControllerBuilder struct { + resource *pb.Resource + selector *selector.Selector + manager *Manager + reconciliationPeriodInMinutes int32 +} + +func NewControllerFor(r *pb.Resource) *ControllerBuilder { + return &ControllerBuilder{ + resource: r, + } +} + +func (c *ControllerBuilder) WithReconcilicationPeriodInMinutes(period int32) *ControllerBuilder { + c.reconciliationPeriodInMinutes = period + return c +} + +func (c *ControllerBuilder) WithSelector(selector *selector.Selector) *ControllerBuilder { + c.selector = selector + return c +} + +func (c *ControllerBuilder) ManagedBy(manager *Manager) *ControllerBuilder { + c.manager = manager + return c +} + +func (c *ControllerBuilder) Create(reconciler string, grpcClient *grpc.RemoteWorkProcessorGrpcClient, + isEnabled func() bool) error { + if c.manager == nil || c.selector == nil || c.reconciliationPeriodInMinutes == 0 || c.resource == nil { + return fmt.Errorf("controller is missing required parameters") + } + + gvk := schema.FromAPIVersionAndKind(c.resource.ApiVersion, c.resource.Kind) + mapping, err := c.manager.dynamicClient.GetGVR(&gvk) + if err != nil { + return fmt.Errorf("failed to resolve resource type from kind %+v: %v", gvk, err) + } + + object := &unstructured.Unstructured{} + object.SetGroupVersionKind(gvk) + + err = ctrl.NewControllerManagedBy(c.manager.delegate). + For(object). + WithEventFilter(c.shouldWatchResource(gvk)). + Complete(createReconciler(c.manager.dynamicClient, mapping, reconciler, grpcClient, + c.reconciliationPeriodInMinutes, isEnabled)) + if err != nil { + return fmt.Errorf("failed to create controller: %v", err) + } + return nil +} + +func (c *ControllerBuilder) shouldWatchResource(gvk schema.GroupVersionKind) predicate.Predicate { + return predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return c.isWatchedResource(e.Object, gvk) + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return c.isWatchedResource(e.ObjectNew, gvk) + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return c.isWatchedResource(e.Object, gvk) + }, + } +} + +func (c *ControllerBuilder) isWatchedResource(o client.Object, gvk schema.GroupVersionKind) bool { + return o != nil && + o.GetObjectKind().GroupVersionKind() == gvk && + o.GetNamespace() == c.resource.GetNamespace().GetValue() && + c.selector.LabelSelector.Matches(labels.Set(o.GetLabels())) && + c.selector.FieldSelector.Matches(o) +} diff --git a/internal/kubernetes/controller/manager-builder.go b/internal/kubernetes/controller/manager-builder.go deleted file mode 100644 index dfc3104..0000000 --- a/internal/kubernetes/controller/manager-builder.go +++ /dev/null @@ -1,110 +0,0 @@ -package controller - -import ( - "log" - "os" - "time" - - pb "github.com/SAP/remote-work-processor/build/proto/generated" - "github.com/SAP/remote-work-processor/internal/cache" - "github.com/SAP/remote-work-processor/internal/kubernetes/dynamic" - "github.com/SAP/remote-work-processor/internal/kubernetes/selector" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/rest" - - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/healthz" - "sigs.k8s.io/controller-runtime/pkg/manager" -) - -var ( - setupLog = ctrl.Log.WithName("setup") -) - -type ControllerManagerBuilder struct { - ControllerManager -} - -type ManagerBuilder interface { - WithConfig(config *rest.Config) *ControllerManagerBuilder - WithOptions(scheme *runtime.Scheme, enableLeaderElection bool) *ControllerManagerBuilder - WithoutLeaderElection() *ControllerManagerBuilder - WithWatchConfiguration(wc *pb.UpdateConfigRequestMessage) *ControllerManagerBuilder - Build() ControllerManager -} - -func CreateManagerBuilder() *ControllerManagerBuilder { - return &ControllerManagerBuilder{ - ControllerManager: ControllerManager{}, - } -} - -func (cm *ControllerManagerBuilder) WithConfig(config *rest.Config) *ControllerManagerBuilder { - cm.config = config - return cm -} - -func (cm *ControllerManagerBuilder) WithOptions(scheme *runtime.Scheme) *ControllerManagerBuilder { - t := 0 * time.Second - cm.options = manager.Options{ - Scheme: scheme, - GracefulShutdownTimeout: &t, - WebhookServer: nil, - HealthProbeBindAddress: "localhost:8811", - MetricsBindAddress: "0", - } - - return cm -} - -func (cm *ControllerManagerBuilder) WithoutLeaderElection() *ControllerManagerBuilder { - cm.options.LeaderElection = false - return cm -} - -func (cm *ControllerManagerBuilder) WithWatchConfiguration(wc *pb.UpdateConfigRequestMessage) *ControllerManagerBuilder { - cm.watchConfig = wc - cm.initSelectors(wc.Resources) - return cm -} - -func (cm *ControllerManagerBuilder) initSelectors(rs map[string]*pb.Resource) { - cm.selectorCache = cache.NewInMemoryCache[string, selector.Selector]() - for k, r := range rs { - cm.selectorCache.Write(k, selector.NewSelector(r.GetLabelSelectors(), r.GetFieldSelectors())) - } -} - -func (cm *ControllerManagerBuilder) Build() ControllerManager { - cm.dynamicClient = buildDynamicClient(cm.config) - cm.manager = buildInternalManager(cm.config, cm.options) - return cm.ControllerManager -} - -func buildDynamicClient(config *rest.Config) *dynamic.DynamicClient { - dc, err := dynamic.NewDynamicClient(config) - if err != nil { - log.Fatalf("unable to create dynamic client: %v\n", err) - } - - return dc -} - -func buildInternalManager(config *rest.Config, options manager.Options) (mgr manager.Manager) { - mgr, err := ctrl.NewManager(config, options) - - if err != nil { - log.Fatalf("unable to start manager: %v\n", err) - } - - if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { - setupLog.Error(err, "unable to set up health check") - os.Exit(1) - } - if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { - setupLog.Error(err, "unable to set up ready check") - os.Exit(1) - } - - return -} diff --git a/internal/kubernetes/controller/manager-engine.go b/internal/kubernetes/controller/manager-engine.go deleted file mode 100644 index 4b080fe..0000000 --- a/internal/kubernetes/controller/manager-engine.go +++ /dev/null @@ -1,69 +0,0 @@ -package controller - -import ( - "context" - "fmt" - "log" - - pb "github.com/SAP/remote-work-processor/build/proto/generated" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/rest" -) - -type ManagerEngine struct { - managerBuilder *ControllerManagerBuilder - context context.Context - cancellation chan struct{} - managerStartedAtLeastOnce bool -} - -func CreateManagerEngine(scheme *runtime.Scheme, config *rest.Config) *ManagerEngine { - builder := CreateManagerBuilder(). - WithConfig(config). - WithOptions(scheme). - WithoutLeaderElection() - - me := &ManagerEngine{ - managerBuilder: builder, - } - - return me -} - -func (e *ManagerEngine) WithContext() { - ctx, cancel := context.WithCancel(context.Background()) - fmt.Println("creating cancellation channel") - e.cancellation = make(chan struct{}) - - go func() { - <-e.cancellation - cancel() - }() - - e.context = ctx -} - -func (e *ManagerEngine) WithWatchConfiguration(wc *pb.UpdateConfigRequestMessage) { - e.managerBuilder.WithWatchConfiguration(wc) -} - -func (e *ManagerEngine) StartManager() error { - fmt.Println("starting manager") - cm := e.managerBuilder.Build() - - if err := cm.CreateControllers(e.context); err != nil { - log.Fatal("unable to create controllers", err) - } - - e.managerStartedAtLeastOnce = true - return cm.manager.Start(e.context) -} - -func (e *ManagerEngine) StopManager() { - fmt.Println("stopping controller manager...") - close(e.cancellation) -} - -func (e *ManagerEngine) ManagerStartedAtLeastOnce() bool { - return e.managerStartedAtLeastOnce -} diff --git a/internal/kubernetes/controller/manager.go b/internal/kubernetes/controller/manager.go index 6df35e3..88c7a29 100644 --- a/internal/kubernetes/controller/manager.go +++ b/internal/kubernetes/controller/manager.go @@ -2,46 +2,36 @@ package controller import ( "context" - + "fmt" pb "github.com/SAP/remote-work-processor/build/proto/generated" - "github.com/SAP/remote-work-processor/internal/cache" + "github.com/SAP/remote-work-processor/internal/grpc" "github.com/SAP/remote-work-processor/internal/kubernetes/dynamic" "github.com/SAP/remote-work-processor/internal/kubernetes/selector" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" + "log" "sigs.k8s.io/controller-runtime/pkg/manager" ) -type ControllerManager struct { - manager manager.Manager - options manager.Options - config *rest.Config - watchConfig *pb.UpdateConfigRequestMessage - dynamicClient *dynamic.DynamicClient - selectorCache cache.Cache[string, selector.Selector] -} - -func (m *ControllerManager) GetClient() client.Client { - return m.manager.GetClient() -} - -func (m *ControllerManager) GetScheme() *runtime.Scheme { - return m.manager.GetScheme() +type Manager struct { + delegate manager.Manager + dynamicClient *dynamic.Client + grpcClient *grpc.RemoteWorkProcessorGrpcClient } -func (m *ControllerManager) CreateControllers(ctx context.Context) error { - for reconciler, resource := range m.watchConfig.GetResources() { - _, err := CreateControllerBuilder(). - For(resource). +func (m *Manager) CreateControllersFor(resources map[string]*pb.Resource, isEnabled func() bool) error { + for reconciler, resource := range resources { + log.Printf("Creating controller for %s/%s watched by %s\n", resource.ApiVersion, resource.Kind, reconciler) + err := NewControllerFor(resource). ManagedBy(m). WithReconcilicationPeriodInMinutes(resource.ReconciliationPeriodInMinutes). - Build(ctx, reconciler) - + WithSelector(selector.NewSelector(resource.GetLabelSelectors(), resource.GetFieldSelectors())). + Create(reconciler, m.grpcClient, isEnabled) if err != nil { - return err + return fmt.Errorf("failed to create controller for %s/%s: %s", resource.ApiVersion, resource.Kind, err) } } - return nil } + +func (m *Manager) Start(ctx context.Context) error { + return m.delegate.Start(ctx) +} diff --git a/internal/kubernetes/controller/manager_builder.go b/internal/kubernetes/controller/manager_builder.go new file mode 100644 index 0000000..202b422 --- /dev/null +++ b/internal/kubernetes/controller/manager_builder.go @@ -0,0 +1,72 @@ +package controller + +import ( + "fmt" + "github.com/SAP/remote-work-processor/internal/grpc" + "github.com/SAP/remote-work-processor/internal/kubernetes/dynamic" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + "log" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/manager" + "time" +) + +type ManagerBuilder struct { + delegate manager.Manager + dynamicClient *dynamic.Client + grpcClient *grpc.RemoteWorkProcessorGrpcClient +} + +func NewManagerBuilder() *ManagerBuilder { + return &ManagerBuilder{} +} + +func (b *ManagerBuilder) SetGrpcClient(client *grpc.RemoteWorkProcessorGrpcClient) *ManagerBuilder { + b.grpcClient = client + return b +} + +func (b *ManagerBuilder) BuildDynamicClient(config *rest.Config) *ManagerBuilder { + dc, err := dynamic.NewDynamicClient(config) + if err != nil { + log.Panicln("Failed to create dynamic client:", err) + } + b.dynamicClient = dc + return b +} + +func (b *ManagerBuilder) BuildInternalManager(config *rest.Config, scheme *runtime.Scheme) *ManagerBuilder { + t := time.Duration(0) + options := manager.Options{ + Scheme: scheme, + GracefulShutdownTimeout: &t, + WebhookServer: nil, + LeaderElection: false, + HealthProbeBindAddress: "localhost:8811", + MetricsBindAddress: "0", + } + + mgr, err := ctrl.NewManager(config, options) + if err != nil { + log.Panicln("Failed to create manager:", err) + } + + // these can fail only if the manager has been started prior to calling them + mgr.AddHealthzCheck("healthz", healthz.Ping) + mgr.AddReadyzCheck("readyz", healthz.Ping) + b.delegate = mgr + return b +} + +func (b *ManagerBuilder) Build() (*Manager, error) { + if b.delegate == nil || b.dynamicClient == nil || b.grpcClient == nil { + return nil, fmt.Errorf("manager is missing required parameters") + } + return &Manager{ + delegate: b.delegate, + dynamicClient: b.dynamicClient, + grpcClient: b.grpcClient, + }, nil +} diff --git a/internal/kubernetes/controller/manager_engine.go b/internal/kubernetes/controller/manager_engine.go new file mode 100644 index 0000000..1773c13 --- /dev/null +++ b/internal/kubernetes/controller/manager_engine.go @@ -0,0 +1,70 @@ +package controller + +import ( + "context" + "fmt" + "log" + + pb "github.com/SAP/remote-work-processor/build/proto/generated" + "github.com/SAP/remote-work-processor/internal/grpc" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" +) + +type ManagerEngine struct { + watchedResources map[string]*pb.Resource + grpcClient *grpc.RemoteWorkProcessorGrpcClient + scheme *runtime.Scheme + config *rest.Config + + running bool + cancelCtx context.CancelFunc +} + +func CreateManagerEngine(scheme *runtime.Scheme, config *rest.Config, client *grpc.RemoteWorkProcessorGrpcClient) *ManagerEngine { + return &ManagerEngine{ + grpcClient: client, + scheme: scheme, + config: config, + } +} + +func (e *ManagerEngine) SetWatchConfiguration(wc *pb.UpdateConfigRequestMessage) { + e.watchedResources = wc.Resources +} + +func (e *ManagerEngine) WatchResources(ctx context.Context, isEnabled func() bool) error { + if len(e.watchedResources) == 0 { + return fmt.Errorf("no resources to watch") + } + + log.Println("Creating manager...") + manager, err := NewManagerBuilder(). + SetGrpcClient(e.grpcClient). + BuildDynamicClient(e.config). + BuildInternalManager(e.config, e.scheme). + Build() + if err != nil { + return err + } + + log.Println("Creating controllers...") + if err := manager.CreateControllersFor(e.watchedResources, isEnabled); err != nil { + return fmt.Errorf("failed to create controllers: %v", err) + } + + ctx, cancel := context.WithCancel(ctx) + e.running = true + e.cancelCtx = cancel + + log.Println("Starting manager...") + return manager.Start(ctx) +} + +func (e *ManagerEngine) Stop() { + e.cancelCtx() +} + +func (e *ManagerEngine) IsRunning() bool { + return e.running +} diff --git a/internal/kubernetes/controller/reconciler.go b/internal/kubernetes/controller/reconciler.go index c7d93d2..b145500 100644 --- a/internal/kubernetes/controller/reconciler.go +++ b/internal/kubernetes/controller/reconciler.go @@ -3,20 +3,20 @@ package controller import ( "context" "encoding/json" - "fmt" + stdLog "log" "time" pb "github.com/SAP/remote-work-processor/build/proto/generated" "github.com/SAP/remote-work-processor/internal/grpc" "github.com/SAP/remote-work-processor/internal/kubernetes/dynamic" - "k8s.io/apimachinery/pkg/api/errors" + kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" dyn "k8s.io/client-go/dynamic" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -25,86 +25,98 @@ const ( ) type WatchConfigReconciler struct { - *dynamic.DynamicClient - *runtime.Scheme + *dynamic.Client mapping *meta.RESTMapping reconciler string reconcilicationPeriodInMinutes time.Duration + grpcClient *grpc.RemoteWorkProcessorGrpcClient + isEnabled func() bool } -func createReconciler(scheme *runtime.Scheme, client *dynamic.DynamicClient, mapping *meta.RESTMapping, reconciler string, reconcilicationPeriodInMinutes int32) reconcile.Reconciler { +func createReconciler(client *dynamic.Client, mapping *meta.RESTMapping, reconciler string, + grpcClient *grpc.RemoteWorkProcessorGrpcClient, reconcilicationPeriodInMinutes int32, isEnabled func() bool) reconcile.Reconciler { return &WatchConfigReconciler{ - Scheme: scheme, - DynamicClient: client, + Client: client, mapping: mapping, reconciler: reconciler, + grpcClient: grpcClient, reconcilicationPeriodInMinutes: time.Duration(reconcilicationPeriodInMinutes) * time.Minute, + isEnabled: isEnabled, } } func (r *WatchConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + if !r.isEnabled() { + return ctrl.Result{RequeueAfter: r.reconcilicationPeriodInMinutes}, nil + } + var resource dyn.ResourceInterface - if r.mapping.Scope.Name() == meta.RESTScopeNameNamespace { // This is the case when reconciled resource is namespaced - resource = r.Client.Resource(r.mapping.Resource).Namespace(req.Namespace) + if r.mapping.Scope.Name() == meta.RESTScopeNameNamespace { + resource = r.GetResourceInterface(r.mapping.Resource).Namespace(req.Namespace) } else { - resource = r.Client.Resource(r.mapping.Resource) + resource = r.GetResourceInterface(r.mapping.Resource) } - u, err := resource.Get(ctx, req.Name, v1.GetOptions{}) + logger := log.FromContext(ctx) + + object, err := resource.Get(ctx, req.Name, v1.GetOptions{}) if err != nil { - if errors.IsNotFound(err) { - fmt.Printf("resource not found. Ignoring the reconciliation, because object could be deleted") + if kerrors.IsNotFound(err) { + logger.Info("resource not found. Ignoring the reconciliation, because object could be deleted") return ctrl.Result{}, nil } - fmt.Printf("failed to get the resource for reconciliation: %v", err) + logger.Error(err, "failed to get the resource for reconciliation") return ctrl.Result{}, err } - if u.GetDeletionTimestamp().IsZero() { - if !controllerutil.ContainsFinalizer(u, FINALIZER) { - controllerutil.AddFinalizer(u, FINALIZER) - if _, err := resource.Update(ctx, u, v1.UpdateOptions{}); err != nil { - fmt.Printf("failed to add resource finalizer: %v", err) + if object.GetDeletionTimestamp().IsZero() { + if !controllerutil.ContainsFinalizer(object, FINALIZER) { + controllerutil.AddFinalizer(object, FINALIZER) + if _, err := resource.Update(ctx, object, v1.UpdateOptions{}); err != nil { + logger.Error(err, "failed to add resource finalizer") return ctrl.Result{}, err } } } else { - if controllerutil.ContainsFinalizer(u, FINALIZER) { - if err := r.sendReconciliationEvent(u, pb.ReconcileEventMessage_RECONCILE_TYPE_DELETE); err != nil { - return ctrl.Result{}, err - } + if err := r.sendReconciliationEvent(object, pb.ReconcileEventMessage_RECONCILE_TYPE_DELETE); err != nil { + return ctrl.Result{}, err + } - controllerutil.RemoveFinalizer(u, FINALIZER) - if _, err := resource.Update(ctx, u, v1.UpdateOptions{}); err != nil { - fmt.Printf("failed to remove resource finalizer: %v", err) + if controllerutil.RemoveFinalizer(object, FINALIZER) { + if _, err := resource.Update(ctx, object, v1.UpdateOptions{}); err != nil { + logger.Error(err, "failed to remove resource finalizer") return ctrl.Result{}, err } } - return ctrl.Result{RequeueAfter: r.reconcilicationPeriodInMinutes}, nil } - if err := r.sendReconciliationEvent(u, pb.ReconcileEventMessage_RECONCILE_TYPE_CREATE_OR_UPDATE); err != nil { + if err := r.sendReconciliationEvent(object, pb.ReconcileEventMessage_RECONCILE_TYPE_CREATE_OR_UPDATE); err != nil { return ctrl.Result{}, err } - return ctrl.Result{RequeueAfter: r.reconcilicationPeriodInMinutes}, nil } -func (r *WatchConfigReconciler) sendReconciliationEvent(u *unstructured.Unstructured, t pb.ReconcileEventMessage_ReconcileType) error { - b, err := json.Marshal(u) +func (r *WatchConfigReconciler) sendReconciliationEvent(object *unstructured.Unstructured, + reconcileType pb.ReconcileEventMessage_ReconcileType) error { + serialized, err := json.Marshal(object) if err != nil { return err } - grpc.Client.Send(newReconciliationEvent( - ofType(t), - withContent(string(b)), - withResourceVersion(u.GetResourceVersion()), + msg := newReconciliationEvent( + ofType(reconcileType), + withContent(string(serialized)), + withResourceVersion(object.GetResourceVersion()), withReconcilerName(r.reconciler), - withReconciliationRequest(u.GetName(), u.GetNamespace()), - ).wrap()) + withReconciliationRequest(object.GetName(), object.GetNamespace()), + ).toProtoMessage() + err = r.grpcClient.Send(msg) + if err != nil { + // the gRPC connection has broken down, need to reestablish or restart the process + stdLog.Printf("could not send reconciliation event message: %v\n", err) + } return nil } diff --git a/internal/kubernetes/controller/reconciliation_event_provider.go b/internal/kubernetes/controller/reconciliation_event_provider.go index 433b9f1..459440e 100644 --- a/internal/kubernetes/controller/reconciliation_event_provider.go +++ b/internal/kubernetes/controller/reconciliation_event_provider.go @@ -5,12 +5,8 @@ import ( "github.com/SAP/remote-work-processor/internal/functional" ) -const ( - OPERATOR_ID_ENV_VAR = "OPERATOR_ID" -) - type ReconciliationEvent struct { - *pb.ClientMessage_ReconcileEvent + msg *pb.ClientMessage_ReconcileEvent } func newReconciliationEvent(opts ...functional.Option[ReconciliationEvent]) *ReconciliationEvent { @@ -27,40 +23,39 @@ func newReconciliationEvent(opts ...functional.Option[ReconciliationEvent]) *Rec return re } -func (re *ReconciliationEvent) wrap() *pb.ClientMessage { - op := &pb.ClientMessage{ - Body: re.ClientMessage_ReconcileEvent, +func (re *ReconciliationEvent) toProtoMessage() *pb.ClientMessage { + return &pb.ClientMessage{ + Body: re.msg, } - return op } func ofType(t pb.ReconcileEventMessage_ReconcileType) functional.Option[ReconciliationEvent] { return func(re *ReconciliationEvent) { - re.ReconcileEvent.Type = t + re.msg.ReconcileEvent.Type = t } } func withContent(c string) functional.Option[ReconciliationEvent] { return func(re *ReconciliationEvent) { - re.ReconcileEvent.Content = c + re.msg.ReconcileEvent.Content = c } } func withReconcilerName(c string) functional.Option[ReconciliationEvent] { return func(re *ReconciliationEvent) { - re.ReconcileEvent.ReconcilerName = c + re.msg.ReconcileEvent.ReconcilerName = c } } func withResourceVersion(c string) functional.Option[ReconciliationEvent] { return func(re *ReconciliationEvent) { - re.ReconcileEvent.ResourceVersion = c + re.msg.ReconcileEvent.ResourceVersion = c } } func withReconciliationRequest(name, namespace string) functional.Option[ReconciliationEvent] { return func(re *ReconciliationEvent) { - re.ReconcileEvent.ReconciliationRequest = &pb.ReconciliationRequest{ + re.msg.ReconcileEvent.ReconciliationRequest = &pb.ReconciliationRequest{ ResourceName: name, ResourceNamespace: &namespace, } diff --git a/internal/kubernetes/dynamic/dynamic_client.go b/internal/kubernetes/dynamic/dynamic_client.go index 6c38d0f..a2bcad8 100644 --- a/internal/kubernetes/dynamic/dynamic_client.go +++ b/internal/kubernetes/dynamic/dynamic_client.go @@ -10,46 +10,29 @@ import ( "k8s.io/client-go/restmapper" ) -type DynamicClient struct { - DiscoveryClient *discovery.DiscoveryClient - Client dynamic.Interface +type Client struct { + mapper meta.RESTMapper + client dynamic.Interface } -func NewDynamicClient(config *rest.Config) (*DynamicClient, error) { - dc := &DynamicClient{} - - if err := dc.createDiscoveryClient(config); err != nil { +func NewDynamicClient(config *rest.Config) (*Client, error) { + discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) + if err != nil { return nil, err } + dc := &Client{} + dc.mapper = restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(discoveryClient)) - if err := dc.createDynamicClient(config); err != nil { + if dc.client, err = dynamic.NewForConfig(config); err != nil { return nil, err } - return dc, nil } -func (dc *DynamicClient) createDynamicClient(config *rest.Config) error { - d, err := dynamic.NewForConfig(config) - if err != nil { - return err - } - - dc.Client = d - return nil -} - -func (dc *DynamicClient) createDiscoveryClient(config *rest.Config) error { - c, err := discovery.NewDiscoveryClientForConfig(config) - if err != nil { - return err - } - - dc.DiscoveryClient = c - return nil +func (dc *Client) GetResourceInterface(resource schema.GroupVersionResource) dynamic.NamespaceableResourceInterface { + return dc.client.Resource(resource) } -func (dc *DynamicClient) GetGVR(gvk *schema.GroupVersionKind) (*meta.RESTMapping, error) { - m := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(dc.DiscoveryClient)) // TODO: Check cache lifecycle and invalidation mechanisms - return m.RESTMapping(gvk.GroupKind(), gvk.Version) +func (dc *Client) GetGVR(gvk *schema.GroupVersionKind) (*meta.RESTMapping, error) { + return dc.mapper.RESTMapping(gvk.GroupKind(), gvk.Version) } diff --git a/internal/kubernetes/engine/engine.go b/internal/kubernetes/engine/engine.go index 2e33eea..b3e6ff5 100644 --- a/internal/kubernetes/engine/engine.go +++ b/internal/kubernetes/engine/engine.go @@ -1,13 +1,13 @@ package engine import ( + "context" pb "github.com/SAP/remote-work-processor/build/proto/generated" ) type ManagerEngine interface { - StartManager() error - StopManager() - ManagerStartedAtLeastOnce() bool - WithWatchConfiguration(wc *pb.UpdateConfigRequestMessage) - WithContext() + SetWatchConfiguration(wc *pb.UpdateConfigRequestMessage) + WatchResources(ctx context.Context, isEnabled func() bool) error + IsRunning() bool + Stop() } diff --git a/internal/kubernetes/metadata/metadata.go b/internal/kubernetes/metadata/metadata.go index 852e77e..7498ec4 100644 --- a/internal/kubernetes/metadata/metadata.go +++ b/internal/kubernetes/metadata/metadata.go @@ -2,76 +2,54 @@ package metadata import ( "fmt" - "log" + "github.com/SAP/remote-work-processor/internal/utils" "os" - "strings" - "sync" ) const ( - OPERATOR_ID_ENV_VAR = "OPERATOR_ID" - ENVIRONMENT_ENV_VAR = "ENVIRONMENT" - INSTANCE_ID_ENV_VAR = "INSTANCE_ID" - IMAGE_ENV_VAR = "IMAGE" - - IMAGE_TAG_SEPARATOR = ":" -) - -var ( - once sync.Once - Metadata RemoteWorkProcessorMetadata + OPERATOR_ID_ENV_VAR = "RWP_OPERATOR_ID" + ENVIRONMENT_ENV_VAR = "RWP_ENVIRONMENT" + INSTANCE_ID_ENV_VAR = "RWP_INSTANCE_ID" + AUTOPI_HOST_ENV_VAR = "AUTOPI_HOSTNAME" + AUTOPI_PORT_ENV_VAR = "AUTOPI_PORT" ) type RemoteWorkProcessorMetadata struct { operatorId string environment string instanceId string - image string + version string + autopiHost string + autopiPort string } -func InitRemoteWorkProcessorMetadata() { - operatorId, err := getEnv(OPERATOR_ID_ENV_VAR) - if err != nil { - log.Fatal(err) - } - - environment, err := getEnv(ENVIRONMENT_ENV_VAR) - if err != nil { - log.Fatal(err) +func LoadMetadata(instanceID, version string) RemoteWorkProcessorMetadata { + value, present := os.LookupEnv(INSTANCE_ID_ENV_VAR) + if present { + instanceID = value } - - instanceId, err := getEnv(INSTANCE_ID_ENV_VAR) - if err != nil { - log.Fatal(err) - } - - image, err := getEnv(IMAGE_ENV_VAR) - if err != nil { - log.Fatal(err) + return RemoteWorkProcessorMetadata{ + operatorId: utils.GetRequiredEnv(OPERATOR_ID_ENV_VAR), + environment: utils.GetRequiredEnv(ENVIRONMENT_ENV_VAR), + instanceId: instanceID, + version: version, + autopiHost: utils.GetRequiredEnv(AUTOPI_HOST_ENV_VAR), + autopiPort: utils.GetRequiredEnv(AUTOPI_PORT_ENV_VAR), } +} - once.Do(func() { - Metadata = RemoteWorkProcessorMetadata{ - operatorId: operatorId, - environment: environment, - instanceId: instanceId, - image: image, - } - }) +func (m RemoteWorkProcessorMetadata) SessionID() string { + return fmt.Sprintf("%s:%s:%s", m.operatorId, m.environment, m.instanceId) } -func (p RemoteWorkProcessorMetadata) Id() string { - return fmt.Sprintf("%s:%s:%s", p.operatorId, p.environment, p.instanceId) +func (m RemoteWorkProcessorMetadata) BinaryVersion() string { + return m.version } -func (p RemoteWorkProcessorMetadata) BinaryVersion() string { - return p.image[strings.LastIndex(p.image, IMAGE_TAG_SEPARATOR)+1:] +func (m RemoteWorkProcessorMetadata) AutoPiHost() string { + return m.autopiHost } -func getEnv(key string) (string, error) { - h, ok := os.LookupEnv(key) - if !ok { - return "", fmt.Errorf("failed to create remote work processor id, because %s must be set", key) - } - return strings.TrimSpace(h), nil +func (m RemoteWorkProcessorMetadata) AutoPiPort() string { + return m.autopiPort } diff --git a/internal/kubernetes/selector/field-selector.go b/internal/kubernetes/selector/field-selector.go index d1d2d37..fc05a57 100644 --- a/internal/kubernetes/selector/field-selector.go +++ b/internal/kubernetes/selector/field-selector.go @@ -1,7 +1,6 @@ package selector import ( - "fmt" "log" "github.com/itchyny/gojq" @@ -15,25 +14,28 @@ type FieldSelector struct { func NewFieldSelector(selectors []string) FieldSelector { if len(selectors) == 0 { - return FieldSelector{ - jqs: []*gojq.Code{}, - } + return FieldSelector{} } - jqs := []*gojq.Code{} + //TODO: + // split on =, == or != + // keep the first elements as keys + // the second elements would be the values to compare with + + var jqs []*gojq.Code for _, s := range selectors { q, err := gojq.Parse(s) if err != nil { // Ignored - fmt.Println(err.Error()) + log.Println(err.Error()) continue } c, err := gojq.Compile(q) if err != nil { // Ignored - fmt.Println(err.Error()) + log.Println(err.Error()) continue } @@ -46,6 +48,11 @@ func NewFieldSelector(selectors []string) FieldSelector { } func (fs *FieldSelector) Matches(o client.Object) bool { + if len(fs.jqs) == 0 { + return true + } + //TODO: support only =, == and != + fields, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o) if err != nil { log.Printf("Failed to convert object to a unstructured one: %v\n", err) @@ -54,7 +61,7 @@ func (fs *FieldSelector) Matches(o client.Object) bool { for _, jq := range fs.jqs { r, ok := jq.Run(fields).Next() - if isFalsy(r) || !ok { + if !ok || isFalsy(r) { return false } } diff --git a/internal/kubernetes/selector/label-selector.go b/internal/kubernetes/selector/label-selector.go index 1e0c482..c2e7d87 100644 --- a/internal/kubernetes/selector/label-selector.go +++ b/internal/kubernetes/selector/label-selector.go @@ -1,7 +1,7 @@ package selector import ( - "fmt" + "log" "k8s.io/apimachinery/pkg/labels" ) @@ -23,7 +23,7 @@ func NewLabelSelector(selectors []string) LabelSelector { r, err := labels.ParseToRequirements(s) if err != nil { // Ignored - fmt.Println(err.Error()) + log.Println(err.Error()) } ls = ls.Add(r[0]) diff --git a/internal/kubernetes/selector/selector.go b/internal/kubernetes/selector/selector.go index a9bf7a6..ff4a18d 100644 --- a/internal/kubernetes/selector/selector.go +++ b/internal/kubernetes/selector/selector.go @@ -5,8 +5,8 @@ type Selector struct { FieldSelector } -func NewSelector(ls []string, fs []string) Selector { - return Selector{ +func NewSelector(ls []string, fs []string) *Selector { + return &Selector{ LabelSelector: NewLabelSelector(ls), FieldSelector: NewFieldSelector(fs), } diff --git a/internal/utils/array/utils.go b/internal/utils/array/utils.go deleted file mode 100644 index 53891d9..0000000 --- a/internal/utils/array/utils.go +++ /dev/null @@ -1,33 +0,0 @@ -package array - -import "github.com/SAP/remote-work-processor/internal/functional" - -func Map[T any, R any](arr []T, m functional.Function[T, R]) (res []R) { - res = make([]R, len(arr)) - for i, e := range arr { - res[i] = m(e) - } - - return -} - -func Filter[T any](arr []T, p functional.Predicate[T]) (filtered []T) { - filtered = []T{} - for _, e := range arr { - if p(e) { - filtered = append(filtered, e) - } - } - - return -} - -func Contains[T comparable](arr []T, searched T) bool { - for _, e := range arr { - if e == searched { - return true - } - } - - return false -} diff --git a/internal/utils/array_utils.go b/internal/utils/array_utils.go new file mode 100644 index 0000000..119d63b --- /dev/null +++ b/internal/utils/array_utils.go @@ -0,0 +1,10 @@ +package utils + +func Contains[T comparable](arr []T, searched T) bool { + for _, e := range arr { + if e == searched { + return true + } + } + return false +} diff --git a/internal/utils/env_utils.go b/internal/utils/env_utils.go new file mode 100644 index 0000000..f3ce56e --- /dev/null +++ b/internal/utils/env_utils.go @@ -0,0 +1,15 @@ +package utils + +import ( + "log" + "os" + "strings" +) + +func GetRequiredEnv(key string) string { + value, present := os.LookupEnv(key) + if !present { + log.Fatalln("failed to load remote work processor metadata: missing", key) + } + return strings.TrimSpace(value) +} diff --git a/internal/utils/json/utils.go b/internal/utils/json_utils.go similarity index 96% rename from internal/utils/json/utils.go rename to internal/utils/json_utils.go index b96cdcc..dbfa0a9 100644 --- a/internal/utils/json/utils.go +++ b/internal/utils/json_utils.go @@ -1,4 +1,4 @@ -package json +package utils import ( "encoding/json" diff --git a/internal/utils/maps/utils.go b/internal/utils/maps/utils.go deleted file mode 100644 index c0c2865..0000000 --- a/internal/utils/maps/utils.go +++ /dev/null @@ -1,39 +0,0 @@ -package maps - -import "github.com/SAP/remote-work-processor/internal/utils/tuple" - -func Pairs[K comparable, V any](m map[K]V) []tuple.Pair[K, V] { - pairs := make([]tuple.Pair[K, V], len(m)) - var i int32 - - for k, v := range m { - pairs[i] = tuple.PairOf(k, v) - i++ - } - - return pairs -} - -func Keys[K comparable, V any](m map[K]V) []K { - keys := make([]K, len(m)) - var i int32 - - for k := range m { - keys[i] = k - i++ - } - - return keys -} - -func Values[K comparable, V any](m map[K]V) []V { - values := make([]V, len(m)) - var i int32 - - for _, v := range m { - values[i] = v - i++ - } - - return values -} diff --git a/internal/utils/set/type.go b/internal/utils/set/type.go deleted file mode 100644 index 5988da9..0000000 --- a/internal/utils/set/type.go +++ /dev/null @@ -1,50 +0,0 @@ -package set - -type Set[E comparable] interface { - Add(e E) bool - Contains(e E) bool - Clear() - IsEmpty() bool - Size() int -} - -type HashSet[E comparable] struct { - m map[E]bool -} - -func Empty[E comparable]() Set[E] { - return &HashSet[E]{ - map[E]bool{}, - } -} - -func New[E comparable](elements ...E) Set[E] { - s := Empty[E]() - - for _, e := range elements { - s.Add(e) - } - - return s -} - -func (hs *HashSet[E]) Add(e E) bool { - return hs.m[e] -} - -func (hs *HashSet[E]) Contains(e E) bool { - _, ok := hs.m[e] - return ok -} - -func (hs *HashSet[E]) Clear() { - hs.m = map[E]bool{} -} - -func (hs *HashSet[E]) IsEmpty() bool { - return len(hs.m) == 0 -} - -func (hs *HashSet[E]) Size() int { - return len(hs.m) -} diff --git a/internal/utils/tuple/pair.go b/internal/utils/tuple/pair.go deleted file mode 100644 index 0d66e67..0000000 --- a/internal/utils/tuple/pair.go +++ /dev/null @@ -1,13 +0,0 @@ -package tuple - -type Pair[K any, V any] struct { - Key K - Value V -} - -func PairOf[K any, V any](k K, v V) Pair[K, V] { - return Pair[K, V]{ - Key: k, - Value: v, - } -}