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, - } -}