diff --git a/cmd/executionspace/main.go b/cmd/executionspace/main.go index f52d5b4..4f33c41 100644 --- a/cmd/executionspace/main.go +++ b/cmd/executionspace/main.go @@ -23,7 +23,7 @@ import ( "runtime/debug" "syscall" - config "github.com/eiffel-community/etos-api/internal/configs/executionspace" + "github.com/eiffel-community/etos-api/internal/config" "github.com/eiffel-community/etos-api/internal/database/etcd" "github.com/eiffel-community/etos-api/internal/executionspace/provider" "github.com/eiffel-community/etos-api/internal/logging" @@ -39,7 +39,7 @@ import ( // main sets up logging and starts up the webservice. func main() { - cfg := config.Get() + cfg := config.NewExecutionSpaceConfig() ctx := context.Background() var hooks []logrus.Hook @@ -120,7 +120,7 @@ func fileLogging(cfg config.Config) logrus.Hook { // remoteLogging starts a new rabbitmq publisher if the rabbitmq parameters are set // Warning: Must call publisher.Close() on the publisher returned from this function -func remoteLogging(cfg config.Config) *rabbitmq.Publisher { +func remoteLogging(cfg config.ExecutionSpaceConfig) *rabbitmq.Publisher { if cfg.RabbitMQHookURL() != "" { if cfg.RabbitMQHookExchangeName() == "" { panic("-rabbitmq_hook_exchange (env:ETOS_RABBITMQ_EXCHANGE) must be set when using -rabbitmq_hook_url (env:ETOS_RABBITMQ_URL)") diff --git a/cmd/iut/main.go b/cmd/iut/main.go index 19cb0bd..65548a3 100644 --- a/cmd/iut/main.go +++ b/cmd/iut/main.go @@ -24,7 +24,7 @@ import ( "syscall" "time" - config "github.com/eiffel-community/etos-api/internal/configs/iut" + "github.com/eiffel-community/etos-api/internal/config" "github.com/eiffel-community/etos-api/internal/logging" server "github.com/eiffel-community/etos-api/internal/server" "github.com/eiffel-community/etos-api/pkg/application" @@ -37,7 +37,7 @@ import ( // main sets up logging and starts up the webserver. func main() { - cfg := config.Get() + cfg := config.NewIUTConfig() ctx := context.Background() var hooks []logrus.Hook diff --git a/cmd/keys/main.go b/cmd/keys/main.go new file mode 100644 index 0000000..2b44685 --- /dev/null +++ b/cmd/keys/main.go @@ -0,0 +1,135 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package main + +import ( + "context" + "net/http" + "os" + "os/signal" + "runtime/debug" + "syscall" + "time" + + auth "github.com/eiffel-community/etos-api/internal/authorization" + "github.com/eiffel-community/etos-api/internal/config" + "github.com/eiffel-community/etos-api/internal/logging" + "github.com/eiffel-community/etos-api/internal/server" + "github.com/eiffel-community/etos-api/pkg/application" + v1alpha "github.com/eiffel-community/etos-api/pkg/keys/v1alpha" + "github.com/sirupsen/logrus" + "github.com/snowzach/rotatefilehook" + "go.elastic.co/ecslogrus" +) + +// main sets up logging and starts up the key webserver. +func main() { + cfg := config.NewKeyConfig() + ctx := context.Background() + + var hooks []logrus.Hook + if fileHook := fileLogging(cfg); fileHook != nil { + hooks = append(hooks, fileHook) + } + logger, err := logging.Setup(cfg.LogLevel(), hooks) + if err != nil { + logrus.Fatal(err.Error()) + } + + hostname, err := os.Hostname() + if err != nil { + logrus.Fatal(err.Error()) + } + log := logger.WithFields(logrus.Fields{ + "hostname": hostname, + "application": "ETOS API Key Server", + "version": vcsRevision(), + "name": "ETOS API", + }) + + pub, err := cfg.PublicKey() + if err != nil { + log.Fatal(err.Error()) + } + priv, err := cfg.PrivateKey() + if err != nil { + log.Fatal(err.Error()) + } + authorizer, err := auth.NewAuthorizer(pub, priv) + if err != nil { + log.Fatal(err.Error()) + } + v1AlphaKeys := v1alpha.New(ctx, cfg, log, authorizer) + defer v1AlphaKeys.Close() + + log.Info("Loading Key routes") + app := application.New(v1AlphaKeys) + srv := server.NewWebService(cfg, log, app) + + done := make(chan os.Signal, 1) + signal.Notify(done, syscall.SIGINT, syscall.SIGTERM) + + go func() { + if err := srv.Start(); err != nil && err != http.ErrServerClosed { + log.Errorf("Webserver shutdown: %+v", err) + } + }() + + sig := <-done + log.Infof("%s received", sig.String()) + + ctx, cancel := context.WithTimeout(ctx, 1*time.Minute) + defer cancel() + + if err := srv.Close(ctx); err != nil { + log.Errorf("Webserver shutdown failed: %+v", err) + } + log.Info("Wait for shutdown to complete") +} + +// fileLogging adds a hook into a slice of hooks, if the filepath configuration is set +func fileLogging(cfg config.Config) logrus.Hook { + if filePath := cfg.LogFilePath(); filePath != "" { + // TODO: Make these parameters configurable. + // NewRotateFileHook cannot return an error which is why it's set to '_'. + rotateFileHook, _ := rotatefilehook.NewRotateFileHook(rotatefilehook.RotateFileConfig{ + Filename: filePath, + MaxSize: 10, // megabytes + MaxBackups: 3, + MaxAge: 0, // days + Level: logrus.DebugLevel, + Formatter: &ecslogrus.Formatter{ + DataKey: "labels", + }, + }) + return rotateFileHook + } + return nil +} + +// vcsRevision returns vcs revision from build info, if any. Otherwise '(unknown)'. +func vcsRevision() string { + buildInfo, ok := debug.ReadBuildInfo() + if !ok { + return "(unknown)" + } + for _, val := range buildInfo.Settings { + if val.Key == "vcs.revision" { + return val.Value + } + } + return "(unknown)" +} diff --git a/cmd/logarea/main.go b/cmd/logarea/main.go index 7ce6c3e..4715812 100644 --- a/cmd/logarea/main.go +++ b/cmd/logarea/main.go @@ -24,7 +24,7 @@ import ( "syscall" "time" - config "github.com/eiffel-community/etos-api/internal/configs/logarea" + "github.com/eiffel-community/etos-api/internal/config" "github.com/eiffel-community/etos-api/internal/logging" "github.com/eiffel-community/etos-api/internal/server" "github.com/eiffel-community/etos-api/pkg/application" @@ -36,7 +36,7 @@ import ( // main sets up logging and starts up the logarea webservice. func main() { - cfg := config.Get() + cfg := config.NewLogAreaConfig() ctx := context.Background() var hooks []logrus.Hook diff --git a/cmd/sse/main.go b/cmd/sse/main.go index c18b87d..02762cf 100644 --- a/cmd/sse/main.go +++ b/cmd/sse/main.go @@ -24,12 +24,17 @@ import ( "syscall" "time" - config "github.com/eiffel-community/etos-api/internal/configs/sse" + auth "github.com/eiffel-community/etos-api/internal/authorization" + "github.com/eiffel-community/etos-api/internal/config" "github.com/eiffel-community/etos-api/internal/logging" "github.com/eiffel-community/etos-api/internal/server" + "github.com/eiffel-community/etos-api/internal/stream" "github.com/eiffel-community/etos-api/pkg/application" v1 "github.com/eiffel-community/etos-api/pkg/sse/v1" v1alpha "github.com/eiffel-community/etos-api/pkg/sse/v1alpha" + v2alpha "github.com/eiffel-community/etos-api/pkg/sse/v2alpha" + "github.com/julienschmidt/httprouter" + rabbitMQStream "github.com/rabbitmq/rabbitmq-stream-go-client/pkg/stream" "github.com/sirupsen/logrus" "github.com/snowzach/rotatefilehook" "go.elastic.co/ecslogrus" @@ -37,7 +42,7 @@ import ( // main sets up logging and starts up the sse webserver. func main() { - cfg := config.Get() + cfg := config.NewSSEConfig() ctx := context.Background() var hooks []logrus.Hook @@ -66,7 +71,37 @@ func main() { v1SSE := v1.New(cfg, log, ctx) defer v1SSE.Close() - app := application.New(v1AlphaSSE, v1SSE) + pub, err := cfg.PublicKey() + if err != nil { + log.Fatal(err.Error()) + } + var app *httprouter.Router + // Only load v2alpha if a public key exists. + if pub != nil { + authorizer, err := auth.NewAuthorizer(pub, nil) + if err != nil { + log.Fatal(err.Error()) + } + + var streamer stream.Streamer + if cfg.RabbitMQURI() != "" { + log.Info("Starting up a RabbitMQStreamer") + streamer, err = stream.NewRabbitMQStreamer(*rabbitMQStream.NewEnvironmentOptions().SetUri(cfg.RabbitMQURI()), log) + } else { + log.Warning("RabbitMQURI is not set, defaulting to FileStreamer") + streamer, err = stream.NewFileStreamer(100*time.Millisecond, log) + } + if err != nil { + log.Fatal(err.Error()) + } + v2AlphaSSE := v2alpha.New(ctx, cfg, log, streamer, authorizer) + defer v2AlphaSSE.Close() + app = application.New(v1AlphaSSE, v1SSE, v2AlphaSSE) + } else { + log.Warning("Public key does not exist, won't enable v2alpha endpoint") + app = application.New(v1AlphaSSE, v1SSE) + } + srv := server.NewWebService(cfg, log, app) done := make(chan os.Signal, 1) diff --git a/go.mod b/go.mod index e85d504..07f49dc 100644 --- a/go.mod +++ b/go.mod @@ -7,20 +7,27 @@ toolchain go1.22.1 require ( github.com/eiffel-community/eiffelevents-sdk-go v0.0.0-20240807115026-5ca5c194b7dc github.com/fernet/fernet-go v0.0.0-20240119011108-303da6aec611 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/jmespath/go-jmespath v0.4.0 github.com/julienschmidt/httprouter v1.3.0 - github.com/machinebox/graphql v0.2.2 github.com/maxcnunes/httpfake v1.2.4 github.com/package-url/packageurl-go v0.1.3 + github.com/rabbitmq/amqp091-go v1.10.0 + github.com/rabbitmq/rabbitmq-stream-go-client v1.4.10 github.com/sethvargo/go-retry v0.3.0 github.com/sirupsen/logrus v1.9.3 github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d github.com/stretchr/testify v1.9.0 go.elastic.co/ecslogrus v1.0.0 - go.etcd.io/etcd/api/v3 v3.5.15 go.etcd.io/etcd/client/v3 v3.5.15 go.etcd.io/etcd/server/v3 v3.5.14 + go.opentelemetry.io/otel v1.20.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 + go.opentelemetry.io/otel/sdk v1.20.0 + go.opentelemetry.io/otel/trace v1.20.0 + k8s.io/api v0.31.1 k8s.io/apimachinery v0.31.1 k8s.io/client-go v0.31.1 ) @@ -46,6 +53,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.4.2 // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -59,21 +67,22 @@ require ( github.com/jonboulle/clockwork v0.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/magefile/mage v1.9.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/matryer/is v1.4.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 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/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.11.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect - github.com/rabbitmq/amqp091-go v1.10.0 // indirect github.com/soheilhy/cmux v0.1.5 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/match v1.1.1 // indirect @@ -82,29 +91,24 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect go.etcd.io/bbolt v1.3.10 // indirect + go.etcd.io/etcd/api/v3 v3.5.15 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect go.etcd.io/etcd/client/v2 v2.305.14 // indirect go.etcd.io/etcd/pkg/v3 v3.5.14 // indirect go.etcd.io/etcd/raft/v3 v3.5.14 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 // indirect - go.opentelemetry.io/otel v1.20.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect go.opentelemetry.io/otel/metric v1.20.0 // indirect - go.opentelemetry.io/otel/sdk v1.20.0 // indirect - go.opentelemetry.io/otel/trace v1.20.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.17.0 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect @@ -114,7 +118,6 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.31.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect diff --git a/go.sum b/go.sum index 5f0df3e..c2a4572 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= -cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= @@ -41,7 +39,6 @@ github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzA github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -49,8 +46,6 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eiffel-community/eiffelevents-sdk-go v0.0.0-20240807115026-5ca5c194b7dc h1:yRg84ReJfuVCJ/TMzfCqL12Aoy4vUSrUUgcuE02mBJo= github.com/eiffel-community/eiffelevents-sdk-go v0.0.0-20240807115026-5ca5c194b7dc/go.mod h1:Lt487E8lrDd/5hkCEyKHU/xZrqDjIgRNIDaoK/F3Yk4= -github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= -github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -61,6 +56,8 @@ github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBF github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/fernet/fernet-go v0.0.0-20240119011108-303da6aec611 h1:JwYtKJ/DVEoIA5dH45OEU7uoryZY/gjd/BQiwwAOImM= github.com/fernet/fernet-go v0.0.0-20240119011108-303da6aec611/go.mod h1:zHMNeYgqrTpKyjawjitDg0Osd1P/FmeA0SZLYK3RfLQ= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -70,10 +67,7 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -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.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -82,19 +76,20 @@ github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= @@ -112,6 +107,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= @@ -128,14 +125,11 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= @@ -167,6 +161,8 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -178,14 +174,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo= -github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA= github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE= github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= -github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/maxcnunes/httpfake v1.2.4 h1:l7s/N7zuG6XpzG+5dUolg5SSoR3hANQxqzAkv+lREko= @@ -201,20 +193,19 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE= -github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM= github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= -github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs= github.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= +github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= +github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -239,10 +230,11 @@ github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3x github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/rabbitmq/rabbitmq-stream-go-client v1.4.10 h1:1kDn/orisEbfMtxdZwWKpxX9+FahnzoRCuGCLZ66fAc= +github.com/rabbitmq/rabbitmq-stream-go-client v1.4.10/go.mod h1:SdWsW0K5FVo8lIx0lCH17wh7RItXEQb8bfpxVlTVqS8= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -255,6 +247,8 @@ github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d h1:4660u5v github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d/go.mod h1:ZLVe3VfhAuMYLYWliGEydMBoRnfib8EFSqkBYu1ck9E= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -334,10 +328,8 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -351,7 +343,6 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -359,15 +350,11 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 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-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= -golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -377,9 +364,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -395,21 +381,15 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -420,16 +400,14 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= -golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= @@ -455,8 +433,6 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -484,37 +460,21 @@ 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= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.28.2 h1:9mpl5mOb6vXZvqbQmankOfPIGiudghwCoLl1EYfUZbw= -k8s.io/api v0.28.2/go.mod h1:RVnJBsjU8tcMq7C3iaRSGMeaKt2TWEUXcpIt/90fjEg= k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= -k8s.io/apimachinery v0.28.2 h1:KCOJLrc6gu+wV1BYgwik4AF4vXOlVJPdiqn0yAWWwXQ= -k8s.io/apimachinery v0.28.2/go.mod h1:RdzF87y/ngqk9H4z3EL2Rppv5jj95vGS/HaFXrLDApU= k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/client-go v0.28.2 h1:DNoYI1vGq0slMBN/SWKMZMw0Rq+0EQW6/AK4v9+3VeY= -k8s.io/client-go v0.28.2/go.mod h1:sMkApowspLuc7omj1FOSUxSoqjr+d5Q0Yc0LOFnYFJY= k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= -k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= -k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/authorization/auth.go b/internal/authorization/auth.go new file mode 100644 index 0000000..ce0b3ec --- /dev/null +++ b/internal/authorization/auth.go @@ -0,0 +1,148 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package auth + +import ( + "context" + "crypto" + "errors" + "fmt" + "net/http" + "time" + + "github.com/eiffel-community/etos-api/internal/authorization/scope" + jwt "github.com/golang-jwt/jwt/v5" + "github.com/julienschmidt/httprouter" +) + +type Authorizer struct { + publicKey crypto.PublicKey + signingKey crypto.PrivateKey +} + +var ( + ErrTokenInvalid = errors.New("invalid token") + ErrTokenExpired = jwt.ErrTokenExpired +) + +// NewAuthorizer loads private and public pem keys and creates a new authorizer. +// The private key can be set to an empty []byte but it would only be possible to +// verify tokens and not create new ones. +func NewAuthorizer(pub, priv []byte) (*Authorizer, error) { + var private crypto.PrivateKey + var err error + // Private key is optional, only needed for signing. + if len(priv) > 0 { + private, err = jwt.ParseEdPrivateKeyFromPEM(priv) + if err != nil { + return nil, err + } + } + public, err := jwt.ParseEdPublicKeyFromPEM(pub) + if err != nil { + return nil, err + } + return &Authorizer{public, private}, nil +} + +// NewToken generates a new JWT for an identifier. +func (a Authorizer) NewToken(identifier string, tokenScope scope.Scope) (string, error) { + if a.signingKey == nil { + return "", errors.New("a private key must be provided to the authorizer to create new tokens.") + } + token := jwt.NewWithClaims( + jwt.SigningMethodEdDSA, + jwt.MapClaims{ + "scope": tokenScope.Format(), // Custom scope type, similar to oauth2. Describes what a subject can do with this token + "sub": identifier, // Subject + "aud": "https://etos", // Audience. The service that can be accessed with this token. + "iss": "https://etos", // Issuer + "iat": time.Now().Unix(), // Issued At + "exp": time.Now().Add(time.Minute * 30).Unix(), // Expiration + }) + return token.SignedString(a.signingKey) +} + +// VerifyToken verifies that a token is properly signed with a specific signing key and has not expired. +func (a Authorizer) VerifyToken(tokenString string) (*jwt.Token, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return a.publicKey, nil + }) + if err != nil { + return nil, err + } + if !token.Valid { + return nil, ErrTokenInvalid + } + return token, nil +} + +// Middleware implements an httprouter middleware to use for verifying authorization header JWTs. +// Scope is added to the context of the request and can be accessed by +// +// s := r.Context().Value("scope") +// tokenScope, ok := s.(scope.Scope) +func (a Authorizer) Middleware( + permittedScope scope.Var, + fn func(http.ResponseWriter, *http.Request, httprouter.Params), +) func(http.ResponseWriter, *http.Request, httprouter.Params) { + return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + tokenString := r.Header.Get("Authorization") + if tokenString == "" { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(w, "Missing authorization header") + return + } + tokenString = tokenString[len("Bearer "):] + token, err := a.VerifyToken(tokenString) + if err != nil { + if errors.Is(err, ErrTokenExpired) { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(w, "token has expired") + return + } + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(w, "invalid token") + return + } + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(w, "no valid claims in token") + return + } + tokenScope, ok := claims["scope"] + if !ok { + w.WriteHeader(http.StatusUnauthorized) + fmt.Fprint(w, "no valid scope in token") + return + } + scopeString, ok := tokenScope.(string) + if !ok { + w.WriteHeader(http.StatusForbidden) + fmt.Fprint(w, "no valid scope in token") + return + } + s := scope.Parse(scopeString) + if !s.Has(permittedScope) { + w.WriteHeader(http.StatusForbidden) + fmt.Fprint(w, "no permission to view this page") + return + } + r = r.WithContext(context.WithValue(r.Context(), "scope", s)) + fn(w, r, ps) + } +} diff --git a/internal/authorization/scope/api.go b/internal/authorization/scope/api.go new file mode 100644 index 0000000..274a846 --- /dev/null +++ b/internal/authorization/scope/api.go @@ -0,0 +1,23 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package scope + +var ( + CreateTestrun Var = "post-testrun" + StopTestrun Var = "delete-testrun" +) + +var GetEnvironment Var = "get-environment" diff --git a/internal/authorization/scope/sse.go b/internal/authorization/scope/sse.go new file mode 100644 index 0000000..6e6483a --- /dev/null +++ b/internal/authorization/scope/sse.go @@ -0,0 +1,21 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package scope + +var ( + StreamSSE Var = "get-sse" + DefineSSE Var = "post-sse" +) diff --git a/internal/authorization/scope/types.go b/internal/authorization/scope/types.go new file mode 100644 index 0000000..df7d01f --- /dev/null +++ b/internal/authorization/scope/types.go @@ -0,0 +1,64 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package scope + +import ( + "strings" +) + +type ( + Var string + // Scope is a slice of actions and resources that a token can do. + // For example the string "post-testrun" means that the action "post" can + // be done on "testrun", i.e. create a new testrun for ETOS. + // The actions are HTTP methods and the resource is a name that the service + // described in the 'aud' claim has knowledge about. + Scope []Var +) + +// Format a scope to be used in a JWT. +func (s Scope) Format() string { + var str []string + for _, sc := range s { + str = append(str, string(sc)) + } + return strings.Join(str, " ") +} + +// Has checks if a scope has a specific action + resource. +func (s Scope) Has(scope Var) bool { + for _, sc := range s { + if scope == sc { + return true + } + } + return false +} + +// Parse a scope string and return a Scope. +func Parse(scope string) Scope { + split := strings.Split(scope, " ") + var s Scope + for _, sc := range split { + s = append(s, Var(sc)) + } + return s +} + +var ( + AnonymousAccess Scope = []Var{CreateTestrun, StopTestrun, StreamSSE} + AdminAccess Scope = []Var{CreateTestrun, StopTestrun, GetEnvironment, DefineSSE, StreamSSE} +) diff --git a/internal/configs/base/config.go b/internal/config/base.go similarity index 71% rename from internal/configs/base/config.go rename to internal/config/base.go index 36c5f67..4e347bb 100644 --- a/internal/configs/base/config.go +++ b/internal/config/base.go @@ -25,69 +25,86 @@ import ( type Config interface { ServiceHost() string ServicePort() string + StripPrefix() string LogLevel() string LogFilePath() string ETOSNamespace() string DatabaseURI() string + PublicKey() ([]byte, error) } -// cfg implements the Config interface. -type cfg struct { +// baseCfg implements the Config interface. +type baseCfg struct { serviceHost string servicePort string + stripPrefix string logLevel string logFilePath string etosNamespace string databaseHost string databasePort string + publicKeyPath string } -// Get creates a config interface based on input parameters or environment variables. -func Get() Config { - var conf cfg +// load the command line vars for a base configuration. +func load() Config { + var conf baseCfg flag.StringVar(&conf.serviceHost, "address", EnvOrDefault("SERVICE_HOST", "127.0.0.1"), "Address to serve API on") flag.StringVar(&conf.servicePort, "port", EnvOrDefault("SERVICE_PORT", "8080"), "Port to serve API on") + flag.StringVar(&conf.stripPrefix, "stripprefix", EnvOrDefault("STRIP_PREFIX", ""), "Strip a URL prefix. Useful when a reverse proxy sets a subpath. I.e. reverse proxy sets /stream as prefix, making the etos API available at /stream/v1/events. In that case we want to set stripprefix to /stream") flag.StringVar(&conf.logLevel, "loglevel", EnvOrDefault("LOGLEVEL", "INFO"), "Log level (TRACE, DEBUG, INFO, WARNING, ERROR, FATAL, PANIC).") flag.StringVar(&conf.logFilePath, "logfilepath", os.Getenv("LOG_FILE_PATH"), "Path, including filename, for the log files to create.") flag.StringVar(&conf.etosNamespace, "etosnamespace", ReadNamespaceOrEnv("ETOS_NAMESPACE"), "Path, including filename, for the log files to create.") flag.StringVar(&conf.databaseHost, "databasehost", EnvOrDefault("ETOS_ETCD_HOST", "etcd-client"), "Host to the database.") flag.StringVar(&conf.databasePort, "databaseport", EnvOrDefault("ETOS_ETCD_PORT", "2379"), "Port to the database.") - - flag.Parse() + flag.StringVar(&conf.publicKeyPath, "publickeypath", os.Getenv("PUBLIC_KEY_PATH"), "Path to a public key to use for verifying JWTs.") return &conf } // ServiceHost returns the host of the service. -func (c *cfg) ServiceHost() string { +func (c *baseCfg) ServiceHost() string { return c.serviceHost } // ServicePort returns the port of the service. -func (c *cfg) ServicePort() string { +func (c *baseCfg) ServicePort() string { return c.servicePort } +// StripPrefix returns the prefix to strip. Empty string if no prefix. +func (c *baseCfg) StripPrefix() string { + return c.stripPrefix +} + // LogLevel returns the log level. -func (c *cfg) LogLevel() string { +func (c *baseCfg) LogLevel() string { return c.logLevel } // LogFilePath returns the path to where log files should be stored, including filename. -func (c *cfg) LogFilePath() string { +func (c *baseCfg) LogFilePath() string { return c.logFilePath } // ETOSNamespace returns the ETOS namespace. -func (c *cfg) ETOSNamespace() string { +func (c *baseCfg) ETOSNamespace() string { return c.etosNamespace } // DatabaseURI returns the URI to the ETOS database. -func (c *cfg) DatabaseURI() string { +func (c *baseCfg) DatabaseURI() string { return fmt.Sprintf("%s:%s", c.databaseHost, c.databasePort) } +// PublicKey reads a public key from disk and returns the content. +func (c *baseCfg) PublicKey() ([]byte, error) { + if c.publicKeyPath == "" { + return nil, nil + } + return os.ReadFile(c.publicKeyPath) +} + // EnvOrDefault will look up key in environment variables and return if it exists, else return the fallback value. func EnvOrDefault(key, fallback string) string { if value, ok := os.LookupEnv(key); ok { diff --git a/internal/configs/sse/config_test.go b/internal/config/base_test.go similarity index 90% rename from internal/configs/sse/config_test.go rename to internal/config/base_test.go index abb00ee..292cd2f 100644 --- a/internal/configs/sse/config_test.go +++ b/internal/config/base_test.go @@ -16,7 +16,6 @@ package config import ( - "os" "testing" "github.com/stretchr/testify/assert" @@ -29,12 +28,12 @@ func TestGet(t *testing.T) { logLevel := "DEBUG" logFilePath := "path/to/a/file" - os.Setenv("SERVICE_HOST", serverHost) - os.Setenv("SERVICE_PORT", port) - os.Setenv("LOGLEVEL", logLevel) - os.Setenv("LOG_FILE_PATH", logFilePath) + t.Setenv("SERVICE_HOST", serverHost) + t.Setenv("SERVICE_PORT", port) + t.Setenv("LOGLEVEL", logLevel) + t.Setenv("LOG_FILE_PATH", logFilePath) - conf, ok := Get().(*cfg) + conf, ok := load().(*baseCfg) assert.Truef(t, ok, "cfg returned from get is not a config interface") assert.Equal(t, port, conf.servicePort) assert.Equal(t, serverHost, conf.serviceHost) @@ -46,7 +45,7 @@ type getter func() string // Test that the getters in the Cfg struct return the values from the struct. func TestGetters(t *testing.T) { - conf := &cfg{ + conf := &baseCfg{ serviceHost: "127.0.0.1", servicePort: "8080", logLevel: "TRACE", @@ -54,7 +53,7 @@ func TestGetters(t *testing.T) { } tests := []struct { name string - cfg *cfg + cfg *baseCfg function getter value string }{ diff --git a/internal/config/executionspace.go b/internal/config/executionspace.go new file mode 100644 index 0000000..e5ab9cd --- /dev/null +++ b/internal/config/executionspace.go @@ -0,0 +1,110 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package config + +import ( + "flag" + "os" + "time" + + "github.com/sirupsen/logrus" +) + +type ExecutionSpaceConfig interface { + Config + StripPrefix() string + Hostname() string + Timeout() time.Duration + ExecutionSpaceWaitTimeout() time.Duration + RabbitMQHookURL() string + RabbitMQHookExchangeName() string + EiffelGoerURL() string +} + +// executionSpaceCfg implements the ExecutionSpaceConfig interface. +type executionSpaceCfg struct { + Config + stripPrefix string + hostname string + timeout time.Duration + executionSpaceWaitTimeout time.Duration + rabbitmqHookURL string + rabbitmqHookExchange string + eiffelGoerURL string +} + +// NewExecutionSpaceConfig creates an executio nspace config interface based on input parameters or environment variables. +func NewExecutionSpaceConfig() ExecutionSpaceConfig { + var conf executionSpaceCfg + + defaultTimeout, err := time.ParseDuration(EnvOrDefault("REQUEST_TIMEOUT", "1m")) + if err != nil { + logrus.Panic(err) + } + + executionSpaceWaitTimeout, err := time.ParseDuration(EnvOrDefault("EXECUTION_SPACE_WAIT_TIMEOUT", "1h")) + if err != nil { + logrus.Panic(err) + } + + flag.StringVar(&conf.stripPrefix, "stripprefix", EnvOrDefault("STRIP_PREFIX", ""), "Strip prefix") + flag.StringVar(&conf.hostname, "hostname", EnvOrDefault("PROVIDER_HOSTNAME", "http://localhost"), "Host to supply to ESR for starting executors") + flag.DurationVar(&conf.timeout, "timeout", defaultTimeout, "Maximum timeout for requests to Execution space provider Service.") + flag.DurationVar(&conf.executionSpaceWaitTimeout, "executionSpaceWaitTimeout", executionSpaceWaitTimeout, "Timeout duration to wait when trying to checkout execution space(s)") + flag.StringVar(&conf.rabbitmqHookURL, "rabbitmq_hook_url", os.Getenv("ETOS_RABBITMQ_URL"), "URL to the ETOS rabbitmq for logs") + flag.StringVar(&conf.rabbitmqHookExchange, "rabbitmq_hook_exchange", os.Getenv("ETOS_RABBITMQ_EXCHANGE"), "Exchange to use for the ETOS rabbitmq for logs") + flag.StringVar(&conf.eiffelGoerURL, "event_repository_host", os.Getenv("EIFFEL_GOER_URL"), "Event repository URL used for Eiffel event lookup") + base := load() + flag.Parse() + conf.Config = base + + return &conf +} + +// StripPrefix returns a prefix that is supposed to be stripped from URL. +func (c *executionSpaceCfg) StripPrefix() string { + return c.stripPrefix +} + +// Hostname returns the hostname to use for executors. +func (c *executionSpaceCfg) Hostname() string { + return c.hostname +} + +// Timeout returns the request timeout for Execution space provider Service API. +func (c *executionSpaceCfg) Timeout() time.Duration { + return c.timeout +} + +// ExecutionSpaceWaitTimeout returns the timeout for checking out execution spaces. +func (c *executionSpaceCfg) ExecutionSpaceWaitTimeout() time.Duration { + return c.executionSpaceWaitTimeout +} + +// RabbitMQHookURL returns the rabbitmq url for ETOS logs +func (c *executionSpaceCfg) RabbitMQHookURL() string { + return c.rabbitmqHookURL +} + +// EventRepositoryURL returns the Eiffel event repository used for event lookups +func (c *executionSpaceCfg) EiffelGoerURL() string { + return c.eiffelGoerURL +} + +// RabbitMQHookExchangeName returns the rabbitmq exchange name used for ETOS logs +func (c *executionSpaceCfg) RabbitMQHookExchangeName() string { + return c.rabbitmqHookExchange +} diff --git a/internal/config/iut.go b/internal/config/iut.go new file mode 100644 index 0000000..008d34d --- /dev/null +++ b/internal/config/iut.go @@ -0,0 +1,29 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package config + +import "flag" + +type IUTConfig interface { + Config +} + +// NewIUTConfig creates an iut config interface based on input parameters or environment variables. +func NewIUTConfig() IUTConfig { + cfg := load() + flag.Parse() + return cfg +} diff --git a/internal/config/keys.go b/internal/config/keys.go new file mode 100644 index 0000000..6a0f044 --- /dev/null +++ b/internal/config/keys.go @@ -0,0 +1,52 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package config + +import ( + "flag" + "os" +) + +type KeyConfig interface { + Config + PrivateKey() ([]byte, error) +} + +// keyCfg implements the KeyConfig interface. +type keyCfg struct { + Config + privateKeyPath string +} + +// NewKeyConcifg creates a key config interface based on input parameters or environment variables. +func NewKeyConfig() KeyConfig { + var conf keyCfg + + flag.StringVar(&conf.privateKeyPath, "privatekeypath", os.Getenv("PRIVATE_KEY_PATH"), "Path to a private key to use for signing JWTs.") + base := load() + flag.Parse() + conf.Config = base + + return &conf +} + +// PrivateKey reads a private key from disk and returns the content. +func (c *keyCfg) PrivateKey() ([]byte, error) { + if c.privateKeyPath == "" { + return nil, nil + } + return os.ReadFile(c.privateKeyPath) +} diff --git a/internal/config/logarea.go b/internal/config/logarea.go new file mode 100644 index 0000000..fb69724 --- /dev/null +++ b/internal/config/logarea.go @@ -0,0 +1,29 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package config + +import "flag" + +type LogAreaConfig interface { + Config +} + +// NewLogAreaConfig creates a log area config interface based on input parameters or environment variables. +func NewLogAreaConfig() LogAreaConfig { + cfg := load() + flag.Parse() + return cfg +} diff --git a/internal/config/sse.go b/internal/config/sse.go new file mode 100644 index 0000000..41534ed --- /dev/null +++ b/internal/config/sse.go @@ -0,0 +1,47 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package config + +import ( + "flag" + "os" +) + +type SSEConfig interface { + Config + RabbitMQURI() string +} + +type sseCfg struct { + Config + rabbitmqURI string +} + +// NewSSEConfig creates a sse config interface based on input parameters or environment variables. +func NewSSEConfig() SSEConfig { + var conf sseCfg + + flag.StringVar(&conf.rabbitmqURI, "rabbitmquri", os.Getenv("ETOS_RABBITMQ_URI"), "URI to the RabbitMQ ") + base := load() + conf.Config = base + flag.Parse() + return &conf +} + +// RabbitMQURI returns the RabbitMQ URI. +func (c *sseCfg) RabbitMQURI() string { + return c.rabbitmqURI +} diff --git a/internal/configs/base/config_test.go b/internal/configs/base/config_test.go deleted file mode 100644 index abb00ee..0000000 --- a/internal/configs/base/config_test.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright Axis Communications AB. -// -// For a full list of individual contributors, please see the commit history. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package config - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -// Test that it is possible to get a Cfg from Get with values taken from environment variables. -func TestGet(t *testing.T) { - port := "8080" - serverHost := "127.0.0.1" - logLevel := "DEBUG" - logFilePath := "path/to/a/file" - - os.Setenv("SERVICE_HOST", serverHost) - os.Setenv("SERVICE_PORT", port) - os.Setenv("LOGLEVEL", logLevel) - os.Setenv("LOG_FILE_PATH", logFilePath) - - conf, ok := Get().(*cfg) - assert.Truef(t, ok, "cfg returned from get is not a config interface") - assert.Equal(t, port, conf.servicePort) - assert.Equal(t, serverHost, conf.serviceHost) - assert.Equal(t, logLevel, conf.logLevel) - assert.Equal(t, logFilePath, conf.logFilePath) -} - -type getter func() string - -// Test that the getters in the Cfg struct return the values from the struct. -func TestGetters(t *testing.T) { - conf := &cfg{ - serviceHost: "127.0.0.1", - servicePort: "8080", - logLevel: "TRACE", - logFilePath: "a/file/path.json", - } - tests := []struct { - name string - cfg *cfg - function getter - value string - }{ - {name: "ServiceHost", cfg: conf, function: conf.ServiceHost, value: conf.serviceHost}, - {name: "ServicePort", cfg: conf, function: conf.ServicePort, value: conf.servicePort}, - {name: "LogLevel", cfg: conf, function: conf.LogLevel, value: conf.logLevel}, - {name: "LogFilePath", cfg: conf, function: conf.LogFilePath, value: conf.logFilePath}, - } - for _, testCase := range tests { - t.Run(testCase.name, func(t *testing.T) { - assert.Equal(t, testCase.value, testCase.function()) - }) - } -} diff --git a/internal/configs/executionspace/config.go b/internal/configs/executionspace/config.go deleted file mode 100644 index 63e4b78..0000000 --- a/internal/configs/executionspace/config.go +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright Axis Communications AB. -// -// For a full list of individual contributors, please see the commit history. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package config - -import ( - "flag" - "fmt" - "os" - "time" - - "github.com/sirupsen/logrus" -) - -// Config interface for retreiving configuration options. -type Config interface { - ServiceHost() string - ServicePort() string - StripPrefix() string - Hostname() string - LogLevel() string - LogFilePath() string - Timeout() time.Duration - ExecutionSpaceWaitTimeout() time.Duration - RabbitMQHookURL() string - RabbitMQHookExchangeName() string - DatabaseURI() string - ETOSNamespace() string - EiffelGoerURL() string -} - -// cfg implements the Config interface. -type cfg struct { - serviceHost string - servicePort string - stripPrefix string - hostname string - logLevel string - logFilePath string - timeout time.Duration - databaseHost string - databasePort string - executionSpaceWaitTimeout time.Duration - rabbitmqHookURL string - rabbitmqHookExchange string - eiffelGoerURL string - etosNamespace string -} - -// Get creates a config interface based on input parameters or environment variables. -func Get() Config { - var conf cfg - - defaultTimeout, err := time.ParseDuration(EnvOrDefault("REQUEST_TIMEOUT", "1m")) - if err != nil { - logrus.Panic(err) - } - - executionSpaceWaitTimeout, err := time.ParseDuration(EnvOrDefault("EXECUTION_SPACE_WAIT_TIMEOUT", "1h")) - if err != nil { - logrus.Panic(err) - } - - flag.StringVar(&conf.serviceHost, "address", EnvOrDefault("SERVICE_HOST", "127.0.0.1"), "Address to serve API on") - flag.StringVar(&conf.servicePort, "port", EnvOrDefault("SERVICE_PORT", "8080"), "Port to serve API on") - flag.StringVar(&conf.stripPrefix, "stripprefix", EnvOrDefault("STRIP_PREFIX", ""), "Strip prefix") - flag.StringVar(&conf.hostname, "hostname", EnvOrDefault("PROVIDER_HOSTNAME", "http://localhost"), "Host to supply to ESR for starting executors") - flag.StringVar(&conf.logLevel, "loglevel", EnvOrDefault("LOGLEVEL", "INFO"), "Log level (TRACE, DEBUG, INFO, WARNING, ERROR, FATAL, PANIC).") - flag.StringVar(&conf.logFilePath, "logfilepath", os.Getenv("LOG_FILE_PATH"), "Path, including filename, for the log files to create.") - flag.DurationVar(&conf.timeout, "timeout", defaultTimeout, "Maximum timeout for requests to Execution space provider Service.") - flag.StringVar(&conf.databaseHost, "database_host", EnvOrDefault("ETOS_ETCD_HOST", "etcd-client"), "Host to ETOS database") - flag.StringVar(&conf.databasePort, "database_port", EnvOrDefault("ETOS_ETCD_PORT", "2379"), "Port to ETOS database") - flag.StringVar(&conf.etosNamespace, "etos_namespace", os.Getenv("ETOS_NAMESPACE"), "Namespace to start testrunner k8s jobs") - flag.DurationVar(&conf.executionSpaceWaitTimeout, "execution space wait timeout", executionSpaceWaitTimeout, "Timeout duration to wait when trying to checkout execution space(s)") - flag.StringVar(&conf.rabbitmqHookURL, "rabbitmq_hook_url", os.Getenv("ETOS_RABBITMQ_URL"), "URL to the ETOS rabbitmq for logs") - flag.StringVar(&conf.rabbitmqHookExchange, "rabbitmq_hook_exchange", os.Getenv("ETOS_RABBITMQ_EXCHANGE"), "Exchange to use for the ETOS rabbitmq for logs") - flag.StringVar(&conf.eiffelGoerURL, "event_repository_host", os.Getenv("EIFFEL_GOER_URL"), "Event repository URL used for Eiffel event lookup") - flag.Parse() - return &conf -} - -// ServiceHost returns the host of the service. -func (c *cfg) ServiceHost() string { - return c.serviceHost -} - -// ServicePort returns the port of the service. -func (c *cfg) ServicePort() string { - return c.servicePort -} - -// StripPrefix returns a prefix that is supposed to be stripped from URL. -func (c *cfg) StripPrefix() string { - return c.stripPrefix -} - -// Hostname returns the hostname to use for executors -func (c *cfg) Hostname() string { - return c.hostname -} - -// LogLevel returns the log level. -func (c *cfg) LogLevel() string { - return c.logLevel -} - -// LogFilePath returns the path to where log files should be stored, including filename. -func (c *cfg) LogFilePath() string { - return c.logFilePath -} - -// Timeout returns the request timeout for Execution space provider Service API. -func (c *cfg) Timeout() time.Duration { - return c.timeout -} - -// ExecutionSpaceWaitTimeout returns the timeout for checking out execution spaces. -func (c *cfg) ExecutionSpaceWaitTimeout() time.Duration { - return c.executionSpaceWaitTimeout -} - -// RabbitMQHookURL returns the rabbitmq url for ETOS logs -func (c *cfg) RabbitMQHookURL() string { - return c.rabbitmqHookURL -} - -// EventRepositoryURL returns the Eiffel event repository used for event lookups -func (c *cfg) EiffelGoerURL() string { - return c.eiffelGoerURL -} - -// RabbitMQHookExchangeName returns the rabbitmq exchange name used for ETOS logs -func (c *cfg) RabbitMQHookExchangeName() string { - return c.rabbitmqHookExchange -} - -// DatabaseURI returns the URI to the ETOS database. -func (c *cfg) DatabaseURI() string { - return fmt.Sprintf("%s:%s", c.databaseHost, c.databasePort) -} - -// ETOSNamespace returns the namespace where k8s jobs shall be deployed. -func (c *cfg) ETOSNamespace() string { - return c.etosNamespace -} - -// EnvOrDefault will look up key in environment variables and return if it exists, else return the fallback value. -func EnvOrDefault(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - return fallback -} diff --git a/internal/configs/executionspace/config_test.go b/internal/configs/executionspace/config_test.go deleted file mode 100644 index a8698a1..0000000 --- a/internal/configs/executionspace/config_test.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright Axis Communications AB. -// -// For a full list of individual contributors, please see the commit history. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package config - -import ( - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -// Test that it is possible to get a Cfg from Get with values taken from environment variables. -func TestGet(t *testing.T) { - port := "8080" - serverHost := "127.0.0.1" - logLevel := "DEBUG" - logFilePath := "path/to/a/file" - timeoutStr := "1m" - - os.Setenv("SERVICE_HOST", serverHost) - os.Setenv("SERVICE_PORT", port) - os.Setenv("LOGLEVEL", logLevel) - os.Setenv("LOG_FILE_PATH", logFilePath) - os.Setenv("REQUEST_TIMEOUT", timeoutStr) - - timeout, _ := time.ParseDuration(timeoutStr) - - conf, ok := Get().(*cfg) - assert.Truef(t, ok, "cfg returned from get is not a config interface") - assert.Equal(t, port, conf.servicePort) - assert.Equal(t, serverHost, conf.serviceHost) - assert.Equal(t, logLevel, conf.logLevel) - assert.Equal(t, logFilePath, conf.logFilePath) - assert.Equal(t, timeout, conf.timeout) -} - -type getter func() string - -// Test that the getters in the Cfg struct return the values from the struct. -func TestGetters(t *testing.T) { - conf := &cfg{ - serviceHost: "127.0.0.1", - servicePort: "8080", - logLevel: "TRACE", - logFilePath: "a/file/path.json", - } - tests := []struct { - name string - cfg *cfg - function getter - value string - }{ - {name: "ServiceHost", cfg: conf, function: conf.ServiceHost, value: conf.serviceHost}, - {name: "ServicePort", cfg: conf, function: conf.ServicePort, value: conf.servicePort}, - {name: "LogLevel", cfg: conf, function: conf.LogLevel, value: conf.logLevel}, - {name: "LogFilePath", cfg: conf, function: conf.LogFilePath, value: conf.logFilePath}, - } - for _, testCase := range tests { - t.Run(testCase.name, func(t *testing.T) { - assert.Equal(t, testCase.value, testCase.function()) - }) - } -} - -// TestTimeoutGetter tests the getter for Timeout. Similar to TestGetters, but since -// Timeout is not a "func() string" we separate its test. -func TestTimeoutGetter(t *testing.T) { - timeout, _ := time.ParseDuration("1m") - conf := &cfg{ - timeout: timeout, - } - assert.Equal(t, conf.timeout, conf.Timeout()) -} diff --git a/internal/configs/iut/config.go b/internal/configs/iut/config.go deleted file mode 100644 index 4b8fb0b..0000000 --- a/internal/configs/iut/config.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2022 Axis Communications AB. -// -// For a full list of individual contributors, please see the commit history. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package config - -import ( - "flag" - "fmt" - "os" -) - -// Config interface for retrieving configuration options. -type Config interface { - ServiceHost() string - ServicePort() string - LogLevel() string - LogFilePath() string - ETOSNamespace() string - DatabaseURI() string -} - -// cfg implements the Config interface. -type cfg struct { - serviceHost string - servicePort string - logLevel string - logFilePath string - etosNamespace string - databaseHost string - databasePort string -} - -// Get creates a config interface based on input parameters or environment variables. -func Get() Config { - var conf cfg - - flag.StringVar(&conf.serviceHost, "address", EnvOrDefault("SERVICE_HOST", "127.0.0.1"), "Address to serve API on") - flag.StringVar(&conf.servicePort, "port", EnvOrDefault("SERVICE_PORT", "8080"), "Port to serve API on") - flag.StringVar(&conf.logLevel, "loglevel", EnvOrDefault("LOGLEVEL", "INFO"), "Log level (TRACE, DEBUG, INFO, WARNING, ERROR, FATAL, PANIC).") - flag.StringVar(&conf.logFilePath, "logfilepath", os.Getenv("LOG_FILE_PATH"), "Path, including filename, for the log files to create.") - flag.StringVar(&conf.databaseHost, "database_host", EnvOrDefault("ETOS_ETCD_HOST", "etcd-client"), "Host to ETOS database") - flag.StringVar(&conf.databasePort, "database_port", EnvOrDefault("ETOS_ETCD_PORT", "2379"), "Port to ETOS database") - flag.Parse() - - return &conf -} - -// ServiceHost returns the host of the service. -func (c *cfg) ServiceHost() string { - return c.serviceHost -} - -// ServicePort returns the port of the service. -func (c *cfg) ServicePort() string { - return c.servicePort -} - -// LogLevel returns the log level. -func (c *cfg) LogLevel() string { - return c.logLevel -} - -// LogFilePath returns the path to where log files should be stored, including filename. -func (c *cfg) LogFilePath() string { - return c.logFilePath -} - -// ETOSNamespace returns the ETOS namespace. -func (c *cfg) ETOSNamespace() string { - return c.etosNamespace -} - -// DatabaseURI returns the URI to the ETOS database. -func (c *cfg) DatabaseURI() string { - return fmt.Sprintf("%s:%s", c.databaseHost, c.databasePort) -} - -// EnvOrDefault will look up key in environment variables and return if it exists, else return the fallback value. -func EnvOrDefault(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - return fallback -} diff --git a/internal/configs/iut/config_test.go b/internal/configs/iut/config_test.go deleted file mode 100644 index b5039da..0000000 --- a/internal/configs/iut/config_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2022 Axis Communications AB. -// -// For a full list of individual contributors, please see the commit history. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package config - -import ( - "fmt" - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -// Test that it is possible to get a Cfg from Get with values taken from environment variables. -func TestGet(t *testing.T) { - port := "8080" - serverHost := "127.0.0.1" - logLevel := "DEBUG" - logFilePath := "path/to/a/file" - databaseHost := "etcd" - databasePort := "12345" - - os.Setenv("SERVICE_HOST", serverHost) - os.Setenv("SERVICE_PORT", port) - os.Setenv("LOGLEVEL", logLevel) - os.Setenv("LOG_FILE_PATH", logFilePath) - os.Setenv("ETOS_ETCD_HOST", databaseHost) - os.Setenv("ETOS_ETCD_PORT", databasePort) - - conf, ok := Get().(*cfg) - assert.Truef(t, ok, "cfg returned from get is not a config interface") - assert.Equal(t, port, conf.servicePort) - assert.Equal(t, serverHost, conf.serviceHost) - assert.Equal(t, logLevel, conf.logLevel) - assert.Equal(t, logFilePath, conf.logFilePath) - assert.Equal(t, databaseHost, conf.databaseHost) - assert.Equal(t, databasePort, conf.databasePort) -} - -type getter func() string - -// Test that the getters in the Cfg struct return the values from the struct. -func TestGetters(t *testing.T) { - conf := &cfg{ - serviceHost: "127.0.0.1", - servicePort: "8080", - logLevel: "TRACE", - logFilePath: "a/file/path.json", - databaseHost: "etcd", - databasePort: "12345", - } - tests := []struct { - name string - cfg *cfg - function getter - value string - }{ - {name: "ServiceHost", cfg: conf, function: conf.ServiceHost, value: conf.serviceHost}, - {name: "ServicePort", cfg: conf, function: conf.ServicePort, value: conf.servicePort}, - {name: "LogLevel", cfg: conf, function: conf.LogLevel, value: conf.logLevel}, - {name: "LogFilePath", cfg: conf, function: conf.LogFilePath, value: conf.logFilePath}, - {name: "DatabaseURI", cfg: conf, function: conf.DatabaseURI, value: fmt.Sprintf("%s:%s", conf.databaseHost, conf.databasePort)}, - } - for _, testCase := range tests { - t.Run(testCase.name, func(t *testing.T) { - assert.Equal(t, testCase.value, testCase.function()) - }) - } -} diff --git a/internal/configs/logarea/config.go b/internal/configs/logarea/config.go deleted file mode 100644 index 36c5f67..0000000 --- a/internal/configs/logarea/config.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright Axis Communications AB. -// -// For a full list of individual contributors, please see the commit history. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package config - -import ( - "flag" - "fmt" - "os" -) - -// Config interface for retreiving configuration options. -type Config interface { - ServiceHost() string - ServicePort() string - LogLevel() string - LogFilePath() string - ETOSNamespace() string - DatabaseURI() string -} - -// cfg implements the Config interface. -type cfg struct { - serviceHost string - servicePort string - logLevel string - logFilePath string - etosNamespace string - databaseHost string - databasePort string -} - -// Get creates a config interface based on input parameters or environment variables. -func Get() Config { - var conf cfg - - flag.StringVar(&conf.serviceHost, "address", EnvOrDefault("SERVICE_HOST", "127.0.0.1"), "Address to serve API on") - flag.StringVar(&conf.servicePort, "port", EnvOrDefault("SERVICE_PORT", "8080"), "Port to serve API on") - flag.StringVar(&conf.logLevel, "loglevel", EnvOrDefault("LOGLEVEL", "INFO"), "Log level (TRACE, DEBUG, INFO, WARNING, ERROR, FATAL, PANIC).") - flag.StringVar(&conf.logFilePath, "logfilepath", os.Getenv("LOG_FILE_PATH"), "Path, including filename, for the log files to create.") - flag.StringVar(&conf.etosNamespace, "etosnamespace", ReadNamespaceOrEnv("ETOS_NAMESPACE"), "Path, including filename, for the log files to create.") - flag.StringVar(&conf.databaseHost, "databasehost", EnvOrDefault("ETOS_ETCD_HOST", "etcd-client"), "Host to the database.") - flag.StringVar(&conf.databasePort, "databaseport", EnvOrDefault("ETOS_ETCD_PORT", "2379"), "Port to the database.") - - flag.Parse() - return &conf -} - -// ServiceHost returns the host of the service. -func (c *cfg) ServiceHost() string { - return c.serviceHost -} - -// ServicePort returns the port of the service. -func (c *cfg) ServicePort() string { - return c.servicePort -} - -// LogLevel returns the log level. -func (c *cfg) LogLevel() string { - return c.logLevel -} - -// LogFilePath returns the path to where log files should be stored, including filename. -func (c *cfg) LogFilePath() string { - return c.logFilePath -} - -// ETOSNamespace returns the ETOS namespace. -func (c *cfg) ETOSNamespace() string { - return c.etosNamespace -} - -// DatabaseURI returns the URI to the ETOS database. -func (c *cfg) DatabaseURI() string { - return fmt.Sprintf("%s:%s", c.databaseHost, c.databasePort) -} - -// EnvOrDefault will look up key in environment variables and return if it exists, else return the fallback value. -func EnvOrDefault(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - return fallback -} - -// ReadNamespaceOrEnv checks if there's a nemspace file inside the container, else returns -// environment variable with envKey as name. -func ReadNamespaceOrEnv(envKey string) string { - inClusterNamespace, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") - if err != nil { - return os.Getenv(envKey) - } - return string(inClusterNamespace) -} diff --git a/internal/configs/logarea/config_test.go b/internal/configs/logarea/config_test.go deleted file mode 100644 index abb00ee..0000000 --- a/internal/configs/logarea/config_test.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright Axis Communications AB. -// -// For a full list of individual contributors, please see the commit history. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package config - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -// Test that it is possible to get a Cfg from Get with values taken from environment variables. -func TestGet(t *testing.T) { - port := "8080" - serverHost := "127.0.0.1" - logLevel := "DEBUG" - logFilePath := "path/to/a/file" - - os.Setenv("SERVICE_HOST", serverHost) - os.Setenv("SERVICE_PORT", port) - os.Setenv("LOGLEVEL", logLevel) - os.Setenv("LOG_FILE_PATH", logFilePath) - - conf, ok := Get().(*cfg) - assert.Truef(t, ok, "cfg returned from get is not a config interface") - assert.Equal(t, port, conf.servicePort) - assert.Equal(t, serverHost, conf.serviceHost) - assert.Equal(t, logLevel, conf.logLevel) - assert.Equal(t, logFilePath, conf.logFilePath) -} - -type getter func() string - -// Test that the getters in the Cfg struct return the values from the struct. -func TestGetters(t *testing.T) { - conf := &cfg{ - serviceHost: "127.0.0.1", - servicePort: "8080", - logLevel: "TRACE", - logFilePath: "a/file/path.json", - } - tests := []struct { - name string - cfg *cfg - function getter - value string - }{ - {name: "ServiceHost", cfg: conf, function: conf.ServiceHost, value: conf.serviceHost}, - {name: "ServicePort", cfg: conf, function: conf.ServicePort, value: conf.servicePort}, - {name: "LogLevel", cfg: conf, function: conf.LogLevel, value: conf.logLevel}, - {name: "LogFilePath", cfg: conf, function: conf.LogFilePath, value: conf.logFilePath}, - } - for _, testCase := range tests { - t.Run(testCase.name, func(t *testing.T) { - assert.Equal(t, testCase.value, testCase.function()) - }) - } -} diff --git a/internal/configs/sse/config.go b/internal/configs/sse/config.go deleted file mode 100644 index 36c5f67..0000000 --- a/internal/configs/sse/config.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright Axis Communications AB. -// -// For a full list of individual contributors, please see the commit history. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package config - -import ( - "flag" - "fmt" - "os" -) - -// Config interface for retreiving configuration options. -type Config interface { - ServiceHost() string - ServicePort() string - LogLevel() string - LogFilePath() string - ETOSNamespace() string - DatabaseURI() string -} - -// cfg implements the Config interface. -type cfg struct { - serviceHost string - servicePort string - logLevel string - logFilePath string - etosNamespace string - databaseHost string - databasePort string -} - -// Get creates a config interface based on input parameters or environment variables. -func Get() Config { - var conf cfg - - flag.StringVar(&conf.serviceHost, "address", EnvOrDefault("SERVICE_HOST", "127.0.0.1"), "Address to serve API on") - flag.StringVar(&conf.servicePort, "port", EnvOrDefault("SERVICE_PORT", "8080"), "Port to serve API on") - flag.StringVar(&conf.logLevel, "loglevel", EnvOrDefault("LOGLEVEL", "INFO"), "Log level (TRACE, DEBUG, INFO, WARNING, ERROR, FATAL, PANIC).") - flag.StringVar(&conf.logFilePath, "logfilepath", os.Getenv("LOG_FILE_PATH"), "Path, including filename, for the log files to create.") - flag.StringVar(&conf.etosNamespace, "etosnamespace", ReadNamespaceOrEnv("ETOS_NAMESPACE"), "Path, including filename, for the log files to create.") - flag.StringVar(&conf.databaseHost, "databasehost", EnvOrDefault("ETOS_ETCD_HOST", "etcd-client"), "Host to the database.") - flag.StringVar(&conf.databasePort, "databaseport", EnvOrDefault("ETOS_ETCD_PORT", "2379"), "Port to the database.") - - flag.Parse() - return &conf -} - -// ServiceHost returns the host of the service. -func (c *cfg) ServiceHost() string { - return c.serviceHost -} - -// ServicePort returns the port of the service. -func (c *cfg) ServicePort() string { - return c.servicePort -} - -// LogLevel returns the log level. -func (c *cfg) LogLevel() string { - return c.logLevel -} - -// LogFilePath returns the path to where log files should be stored, including filename. -func (c *cfg) LogFilePath() string { - return c.logFilePath -} - -// ETOSNamespace returns the ETOS namespace. -func (c *cfg) ETOSNamespace() string { - return c.etosNamespace -} - -// DatabaseURI returns the URI to the ETOS database. -func (c *cfg) DatabaseURI() string { - return fmt.Sprintf("%s:%s", c.databaseHost, c.databasePort) -} - -// EnvOrDefault will look up key in environment variables and return if it exists, else return the fallback value. -func EnvOrDefault(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - return fallback -} - -// ReadNamespaceOrEnv checks if there's a nemspace file inside the container, else returns -// environment variable with envKey as name. -func ReadNamespaceOrEnv(envKey string) string { - inClusterNamespace, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") - if err != nil { - return os.Getenv(envKey) - } - return string(inClusterNamespace) -} diff --git a/internal/database/etcd/etcd.go b/internal/database/etcd/etcd.go index c57fdaa..f54e814 100644 --- a/internal/database/etcd/etcd.go +++ b/internal/database/etcd/etcd.go @@ -22,7 +22,7 @@ import ( "io" "time" - config "github.com/eiffel-community/etos-api/internal/configs/base" + "github.com/eiffel-community/etos-api/internal/config" "github.com/eiffel-community/etos-api/internal/database" "github.com/google/uuid" "github.com/sirupsen/logrus" diff --git a/internal/executionspace/provider/kubernetes.go b/internal/executionspace/provider/kubernetes.go index 97c5b71..9aa833c 100644 --- a/internal/executionspace/provider/kubernetes.go +++ b/internal/executionspace/provider/kubernetes.go @@ -19,7 +19,7 @@ import ( "fmt" "sync" - config "github.com/eiffel-community/etos-api/internal/configs/executionspace" + "github.com/eiffel-community/etos-api/internal/config" "github.com/eiffel-community/etos-api/internal/database" "github.com/eiffel-community/etos-api/internal/executionspace/executor" ) @@ -29,7 +29,7 @@ type Kubernetes struct { } // New creates a copy of a Kubernetes provider -func (k Kubernetes) New(db database.Opener, cfg config.Config) Provider { +func (k Kubernetes) New(db database.Opener, cfg config.ExecutionSpaceConfig) Provider { return &Kubernetes{ providerCore{ db: db, diff --git a/internal/executionspace/provider/provider.go b/internal/executionspace/provider/provider.go index a73a4ff..6a5ec35 100644 --- a/internal/executionspace/provider/provider.go +++ b/internal/executionspace/provider/provider.go @@ -19,7 +19,7 @@ import ( "context" "sync" - config "github.com/eiffel-community/etos-api/internal/configs/executionspace" + "github.com/eiffel-community/etos-api/internal/config" "github.com/eiffel-community/etos-api/internal/database" "github.com/eiffel-community/etos-api/internal/executionspace/executor" "github.com/eiffel-community/etos-api/pkg/executionspace/executionspace" @@ -28,7 +28,7 @@ import ( ) type Provider interface { - New(database.Opener, config.Config) Provider + New(database.Opener, config.ExecutionSpaceConfig) Provider Status(*logrus.Entry, context.Context, uuid.UUID) (*executionspace.ExecutionSpace, error) Checkout(*logrus.Entry, context.Context, ExecutorConfig) Checkin(*logrus.Entry, context.Context, []executionspace.ExecutorSpec) error @@ -52,7 +52,7 @@ type ExecutorConfig struct { // be included into another struct that implements the rest of the interface. type providerCore struct { db database.Opener - cfg config.Config + cfg config.ExecutionSpaceConfig url string active *sync.WaitGroup executor executor.Executor diff --git a/internal/kubernetes/kubernetes.go b/internal/kubernetes/kubernetes.go index cb8bee7..a43a955 100644 --- a/internal/kubernetes/kubernetes.go +++ b/internal/kubernetes/kubernetes.go @@ -19,7 +19,7 @@ import ( "context" "fmt" - config "github.com/eiffel-community/etos-api/internal/configs/base" + "github.com/eiffel-community/etos-api/internal/config" "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" diff --git a/internal/server/server.go b/internal/server/server.go index 79fd9f3..6c8cba7 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -20,7 +20,7 @@ import ( "fmt" "net/http" - config "github.com/eiffel-community/etos-api/internal/configs/base" + "github.com/eiffel-community/etos-api/internal/config" "github.com/sirupsen/logrus" ) @@ -39,6 +39,9 @@ type WebService struct { // NewWebService creates a new Server of the webservice type. func NewWebService(cfg config.Config, log *logrus.Entry, handler http.Handler) Server { + if cfg.StripPrefix() != "" { + handler = http.StripPrefix(cfg.StripPrefix(), handler) + } webservice := &WebService{ server: &http.Server{ Addr: fmt.Sprintf("%s:%s", cfg.ServiceHost(), cfg.ServicePort()), @@ -52,7 +55,7 @@ func NewWebService(cfg config.Config, log *logrus.Entry, handler http.Handler) S // Start a webservice and block until closed or crashed. func (s *WebService) Start() error { - s.logger.Infof("Starting webservice listening on %s:%s", s.cfg.ServiceHost(), s.cfg.ServicePort()) + s.logger.Infof("Starting webservice listening on %s:%s%s", s.cfg.ServiceHost(), s.cfg.ServicePort(), s.cfg.StripPrefix()) return s.server.ListenAndServe() } diff --git a/internal/stream/file.go b/internal/stream/file.go new file mode 100644 index 0000000..4d06a2c --- /dev/null +++ b/internal/stream/file.go @@ -0,0 +1,136 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package stream + +import ( + "bufio" + "context" + "errors" + "io" + "os" + "time" + + "github.com/sirupsen/logrus" +) + +// FileStreamer will create a stream that reads from a file and publishes them +// to a consumer. +type FileStreamer struct { + interval time.Duration + logger *logrus.Entry +} + +func NewFileStreamer(interval time.Duration, logger *logrus.Entry) (Streamer, error) { + return &FileStreamer{interval: interval, logger: logger}, nil +} + +// CreateStream does nothing. +func (s *FileStreamer) CreateStream(ctx context.Context, logger *logrus.Entry, name string) error { + return nil +} + +// NewStream creates a new stream struct to consume from. +func (s *FileStreamer) NewStream(ctx context.Context, logger *logrus.Entry, name string) (Stream, error) { + file, err := os.Open(name) + if err != nil { + return nil, err + } + return &FileStream{ctx: ctx, file: file, interval: s.interval, logger: logger}, nil +} + +// Close does nothing. +func (s *FileStreamer) Close() {} + +// FileStream is a structure implementing the Stream interface. Used to consume events +// from a file. +type FileStream struct { + ctx context.Context + logger *logrus.Entry + file io.ReadCloser + interval time.Duration + offset int + channel chan<- []byte + filter []string +} + +// WithChannel adds a channel for receiving events from the stream. If no +// channel is added, then events will be logged. +func (s *FileStream) WithChannel(ch chan<- []byte) Stream { + s.channel = ch + return s +} + +// WithOffset adds an offset to the file stream. -1 means start from the beginning. +func (s *FileStream) WithOffset(offset int) Stream { + s.logger.Warning("offset is not yet supported by file stream") + return s +} + +// WithFilter adds a filter to the file stream. +func (s *FileStream) WithFilter(filter []string) Stream { + s.logger.Warning("filter is not yet supported by file stream") + return s +} + +// Consume will start consuming the file, non blocking. A channel is returned where +// an error is sent when the consumer closes down. +func (s *FileStream) Consume(ctx context.Context) (<-chan error, error) { + closed := make(chan error) + go func() { + scanner := bufio.NewReader(s.file) + interval := time.NewTicker(s.interval) + for { + select { + case <-ctx.Done(): + closed <- ctx.Err() + return + case <-interval.C: + var isPrefix bool = true + var err error + var line []byte + var event []byte + + for isPrefix && err == nil { + line, isPrefix, err = scanner.ReadLine() + event = append(event, line...) + } + if err != nil { + // Don't close the stream just because the file is empty. + if errors.Is(err, io.EOF) { + continue + } + closed <- err + return + } + if s.channel != nil { + s.channel <- event + } else { + s.logger.Info(event) + } + } + } + }() + return closed, nil +} + +// Close the file. +func (s *FileStream) Close() { + if s.file != nil { + if err := s.file.Close(); err != nil { + s.logger.WithError(err).Error("failed to close the file") + } + } +} diff --git a/internal/stream/rabbitmq.go b/internal/stream/rabbitmq.go new file mode 100644 index 0000000..2d625c7 --- /dev/null +++ b/internal/stream/rabbitmq.go @@ -0,0 +1,187 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package stream + +import ( + "context" + "errors" + "fmt" + "log" + "time" + + "github.com/rabbitmq/rabbitmq-stream-go-client/pkg/amqp" + "github.com/rabbitmq/rabbitmq-stream-go-client/pkg/stream" + "github.com/sirupsen/logrus" +) + +const IgnoreUnfiltered = false + +// RabbitMQStreamer is a struct representing a single RabbitMQ connection. From a new connection new streams may be created. +// Normal case is to have a single connection with multiple streams. If multiple connections are needed then multiple +// instances of the program should be run. +type RabbitMQStreamer struct { + environment *stream.Environment + logger *logrus.Entry +} + +// NewRabbitMQStreamer creates a new RabbitMQ streamer. Only a single connection should be created. +func NewRabbitMQStreamer(options stream.EnvironmentOptions, logger *logrus.Entry) (Streamer, error) { + env, err := stream.NewEnvironment(&options) + if err != nil { + log.Fatal(err) + } + return &RabbitMQStreamer{environment: env, logger: logger}, err +} + +// CreateStream creates a new RabbitMQ stream. +func (s *RabbitMQStreamer) CreateStream(ctx context.Context, logger *logrus.Entry, name string) error { + logger.Info("Defining a new stream") + // This will create the stream if not already created. + return s.environment.DeclareStream(name, + &stream.StreamOptions{ + // TODO: More sane numbers + MaxLengthBytes: stream.ByteCapacity{}.GB(2), + MaxAge: time.Second * 10, + }, + ) +} + +// NewStream creates a new stream struct to consume from. +func (s *RabbitMQStreamer) NewStream(ctx context.Context, logger *logrus.Entry, name string) (Stream, error) { + exists, err := s.environment.StreamExists(name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.New("no stream exists, cannot stream events") + } + options := stream.NewConsumerOptions(). + SetClientProvidedName(name). + SetConsumerName(name). + SetCRCCheck(false). + SetOffset(stream.OffsetSpecification{}.First()) + return &RabbitMQStream{ctx: ctx, logger: logger, streamName: name, environment: s.environment, options: options}, nil +} + +// Close the RabbitMQ connection. +func (s *RabbitMQStreamer) Close() { + err := s.environment.Close() + if err != nil { + s.logger.WithError(err).Error("failed to close RabbitMQStreamer") + } +} + +// RabbitMQStream is a structure implementing the Stream interface. Used to consume events +// from a RabbitMQ stream. +type RabbitMQStream struct { + ctx context.Context + logger *logrus.Entry + streamName string + environment *stream.Environment + options *stream.ConsumerOptions + consumer *stream.Consumer + channel chan<- []byte + filter []string +} + +// WithChannel adds a channel for receiving events from the stream. If no +// channel is added, then events will be logged. +func (s *RabbitMQStream) WithChannel(ch chan<- []byte) Stream { + s.channel = ch + return s +} + +// WithOffset adds an offset to the RabbitMQ stream. -1 means start from the beginning. +func (s *RabbitMQStream) WithOffset(offset int) Stream { + if offset == -1 { + s.options = s.options.SetOffset(stream.OffsetSpecification{}.First()) + } else { + s.options = s.options.SetOffset(stream.OffsetSpecification{}.Offset(int64(offset))) + } + return s +} + +// WithFilter adds a filter to the RabbitMQ stream. +func (s *RabbitMQStream) WithFilter(filter []string) Stream { + s.filter = filter + if len(filter) > 0 { + s.options = s.options.SetFilter(stream.NewConsumerFilter(filter, IgnoreUnfiltered, s.postFilter)) + } + return s +} + +// Consume will start consuming the RabbitMQ stream, non blocking. A channel is returned where +// an error is sent when the consumer closes down. +func (s *RabbitMQStream) Consume(ctx context.Context) (<-chan error, error) { + handler := func(_ stream.ConsumerContext, message *amqp.Message) { + for _, d := range message.Data { + if s.channel != nil { + s.channel <- d + } else { + s.logger.Debug(d) + } + } + } + consumer, err := s.environment.NewConsumer(s.streamName, handler, s.options) + if err != nil { + return nil, err + } + s.consumer = consumer + closed := make(chan error) + go s.notifyClose(ctx, closed) + return closed, nil +} + +// notifyClose will keep track of context and the notify close channel from RabbitMQ and send +// error on a channel. +func (s *RabbitMQStream) notifyClose(ctx context.Context, ch chan<- error) { + closed := s.consumer.NotifyClose() + select { + case <-ctx.Done(): + ch <- ctx.Err() + case event := <-closed: + ch <- event.Err + } +} + +// Close the RabbitMQ stream consumer. +func (s *RabbitMQStream) Close() { + if s.consumer != nil { + if err := s.consumer.Close(); err != nil { + s.logger.WithError(err).Error("failed to close rabbitmq consumer") + } + } +} + +// postFilter applies client side filtering on all messages received from the RabbitMQ stream. +// The RabbitMQ server-side filtering is not perfect and will let through a few messages that don't +// match the filter, this is expected as the RabbitMQ unit of delivery is the chunk and there may +// be multiple messages in a chunk and those messages are not filtered. +func (s *RabbitMQStream) postFilter(message *amqp.Message) bool { + if s.filter == nil { + return true // Unfiltered + } + identifier := message.ApplicationProperties["identifier"] + eventType := message.ApplicationProperties["type"] + eventMeta := message.ApplicationProperties["meta"] + name := fmt.Sprintf("%s.%s.%s", identifier, eventType, eventMeta) + for _, filter := range s.filter { + if name == filter { + return true + } + } + return false +} diff --git a/internal/stream/stream.go b/internal/stream/stream.go new file mode 100644 index 0000000..c88bbe3 --- /dev/null +++ b/internal/stream/stream.go @@ -0,0 +1,36 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package stream + +import ( + "context" + + "github.com/sirupsen/logrus" +) + +type Streamer interface { + NewStream(context.Context, *logrus.Entry, string) (Stream, error) + CreateStream(context.Context, *logrus.Entry, string) error + Close() +} + +type Stream interface { + WithChannel(chan<- []byte) Stream + WithOffset(int) Stream + WithFilter([]string) Stream + Consume(context.Context) (<-chan error, error) + Close() +} diff --git a/pkg/executionspace/v1alpha/executor.go b/pkg/executionspace/v1alpha/executor.go index e98f4e1..eafe573 100644 --- a/pkg/executionspace/v1alpha/executor.go +++ b/pkg/executionspace/v1alpha/executor.go @@ -24,7 +24,7 @@ import ( "time" "github.com/eiffel-community/eiffelevents-sdk-go" - config "github.com/eiffel-community/etos-api/internal/configs/executionspace" + "github.com/eiffel-community/etos-api/internal/config" "github.com/eiffel-community/etos-api/internal/eventrepository" "github.com/eiffel-community/etos-api/internal/executionspace/executor" "github.com/eiffel-community/etos-api/pkg/executionspace/executionspace" @@ -176,7 +176,7 @@ type state struct { } // getSubSuite gets a sub suite from event repository -func (s *state) getSubSuite(ctx context.Context, cfg config.Config) (*eiffelevents.TestSuiteStartedV3, error) { +func (s *state) getSubSuite(ctx context.Context, cfg config.ExecutionSpaceConfig) (*eiffelevents.TestSuiteStartedV3, error) { if s.environment == nil { event, err := eventrepository.EnvironmentDefined(ctx, cfg.EiffelGoerURL(), s.ExecutorSpec.Instructions.Environment["ENVIRONMENT_ID"]) if err != nil { @@ -202,7 +202,7 @@ func (s *state) getSubSuite(ctx context.Context, cfg config.Config) (*eiffeleven } // waitStart waits for a job to start completely -func (s *state) waitStart(ctx context.Context, cfg config.Config, logger *logrus.Entry, executor executor.Executor) error { +func (s *state) waitStart(ctx context.Context, cfg config.ExecutionSpaceConfig, logger *logrus.Entry, executor executor.Executor) error { var event *eiffelevents.TestSuiteStartedV3 var err error if err = retry.Constant(ctx, 5*time.Second, func(ctx context.Context) error { diff --git a/pkg/executionspace/v1alpha/provider.go b/pkg/executionspace/v1alpha/provider.go index 0bbfd69..a03bbfd 100644 --- a/pkg/executionspace/v1alpha/provider.go +++ b/pkg/executionspace/v1alpha/provider.go @@ -28,7 +28,7 @@ import ( "sync" "github.com/eiffel-community/eiffelevents-sdk-go" - config "github.com/eiffel-community/etos-api/internal/configs/executionspace" + "github.com/eiffel-community/etos-api/internal/config" "github.com/eiffel-community/etos-api/internal/executionspace/provider" "github.com/eiffel-community/etos-api/pkg/application" httperrors "github.com/eiffel-community/etos-api/pkg/executionspace/errors" @@ -57,14 +57,14 @@ var ( type ProviderServiceApplication struct { logger *logrus.Entry - cfg config.Config + cfg config.ExecutionSpaceConfig provider provider.Provider wg *sync.WaitGroup } type ProviderServiceHandler struct { logger *logrus.Entry - cfg config.Config + cfg config.ExecutionSpaceConfig provider provider.Provider wg *sync.WaitGroup } @@ -150,7 +150,7 @@ func (a *ProviderServiceApplication) Close() { } // New returns a new ProviderServiceApplication object/struct -func New(cfg config.Config, log *logrus.Entry, provider provider.Provider, ctx context.Context) application.Application { +func New(cfg config.ExecutionSpaceConfig, log *logrus.Entry, provider provider.Provider, ctx context.Context) application.Application { return &ProviderServiceApplication{ logger: log, cfg: cfg, diff --git a/pkg/iut/v1alpha1/v1alpha1.go b/pkg/iut/v1alpha1/v1alpha1.go index 53f356c..b4ad7f1 100644 --- a/pkg/iut/v1alpha1/v1alpha1.go +++ b/pkg/iut/v1alpha1/v1alpha1.go @@ -25,7 +25,7 @@ import ( "time" eiffelevents "github.com/eiffel-community/eiffelevents-sdk-go" - config "github.com/eiffel-community/etos-api/internal/configs/iut" + "github.com/eiffel-community/etos-api/internal/config" "github.com/eiffel-community/etos-api/pkg/application" packageurl "github.com/package-url/packageurl-go" clientv3 "go.etcd.io/etcd/client/v3" @@ -37,20 +37,19 @@ import ( type V1Alpha1Application struct { logger *logrus.Entry - cfg config.Config + cfg config.IUTConfig database *clientv3.Client wg *sync.WaitGroup } type V1Alpha1Handler struct { logger *logrus.Entry - cfg config.Config + cfg config.IUTConfig database *clientv3.Client wg *sync.WaitGroup } -type Dataset struct { -} +type Dataset struct{} // RespondWithJSON writes a JSON response with a status code to the HTTP ResponseWriter. func RespondWithJSON(w http.ResponseWriter, code int, payload interface{}) { @@ -72,7 +71,7 @@ func (a *V1Alpha1Application) Close() { } // New returns a new V1Alpha1Application object/struct -func New(cfg config.Config, log *logrus.Entry, ctx context.Context, cli *clientv3.Client) application.Application { +func New(cfg config.IUTConfig, log *logrus.Entry, ctx context.Context, cli *clientv3.Client) application.Application { return &V1Alpha1Application{ logger: log, cfg: cfg, diff --git a/pkg/keys/v1alpha/keys.go b/pkg/keys/v1alpha/keys.go new file mode 100644 index 0000000..316dd0f --- /dev/null +++ b/pkg/keys/v1alpha/keys.go @@ -0,0 +1,133 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package keys + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + auth "github.com/eiffel-community/etos-api/internal/authorization" + "github.com/eiffel-community/etos-api/internal/authorization/scope" + "github.com/eiffel-community/etos-api/internal/config" + "github.com/eiffel-community/etos-api/pkg/application" + "github.com/julienschmidt/httprouter" + + "github.com/sirupsen/logrus" +) + +const pingInterval = 15 * time.Second + +type Application struct { + logger *logrus.Entry + cfg config.KeyConfig + ctx context.Context + cancel context.CancelFunc + authorizer *auth.Authorizer +} + +type Handler struct { + logger *logrus.Entry + cfg config.KeyConfig + ctx context.Context + authorizer *auth.Authorizer +} + +// Close cancels the application context. +func (a *Application) Close() { + a.cancel() +} + +// New returns a new Application object/struct. +func New(ctx context.Context, cfg config.KeyConfig, log *logrus.Entry, authorizer *auth.Authorizer) application.Application { + ctx, cancel := context.WithCancel(ctx) + return &Application{ + logger: log, + cfg: cfg, + ctx: ctx, + cancel: cancel, + authorizer: authorizer, + } +} + +// LoadRoutes loads all the v2alpha routes. +func (a Application) LoadRoutes(router *httprouter.Router) { + handler := &Handler{a.logger, a.cfg, a.ctx, a.authorizer} + router.GET("/v1alpha/selftest/ping", handler.Selftest) + router.POST("/v1alpha/generate", handler.CreateNew) +} + +// Selftest is a handler to just return 204. +func (h Handler) Selftest(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(http.StatusNoContent) +} + +// KeyResponse describes the response from the key server. +type KeyResponse struct { + Token string `json:"token,omitempty"` + Error string `json:"error,omitempty"` +} + +// KeyRequest describes the request to the key server. +type KeyRequest struct { + Scope string `json:"scope"` + Identity string `json:"identity"` +} + +// CreateNew is a handler that can create new access tokens. +func (h Handler) CreateNew(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + identifier := ps.ByName("identifier") + // Making it possible for us to correlate logs to a specific connection + logger := h.logger.WithField("identifier", identifier) + + var keyRequest KeyRequest + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&keyRequest); err != nil { + logger.WithError(err).Warning("failed to decode json") + w.WriteHeader(http.StatusBadRequest) + response, _ := json.Marshal(KeyResponse{Error: "must provide one or several scopes to access"}) + _, _ = w.Write(response) + return + } + logger.Info(keyRequest.Identity) + requestedScope := scope.Parse(keyRequest.Scope) + for _, s := range requestedScope { + if !scope.AnonymousAccess.Has(s) { + w.WriteHeader(http.StatusForbidden) + response, _ := json.Marshal(KeyResponse{Error: fmt.Sprintf("not allowed to request the '%s' scope", s)}) + _, _ = w.Write(response) + return + } + } + + w.Header().Set("Content-Type", "application/json") + token, err := h.authorizer.NewToken(identifier, requestedScope) + if err != nil { + logger.WithError(err).Warning("failed to generate token") + w.WriteHeader(http.StatusInternalServerError) + response, _ := json.Marshal(KeyResponse{Error: "could not create a new token"}) + _, _ = w.Write(response) + return + } + logger.Info("generated a new key") + w.WriteHeader(http.StatusOK) + response, _ := json.Marshal(KeyResponse{Token: token}) + _, _ = w.Write(response) +} diff --git a/pkg/logarea/v1alpha/logarea.go b/pkg/logarea/v1alpha/logarea.go index 3b016c3..6ad8bd3 100644 --- a/pkg/logarea/v1alpha/logarea.go +++ b/pkg/logarea/v1alpha/logarea.go @@ -25,7 +25,7 @@ import ( "runtime" "time" - config "github.com/eiffel-community/etos-api/internal/configs/logarea" + "github.com/eiffel-community/etos-api/internal/config" "github.com/eiffel-community/etos-api/pkg/application" "github.com/julienschmidt/httprouter" "github.com/sirupsen/logrus" @@ -40,14 +40,14 @@ const ( type LogAreaApplication struct { logger *logrus.Entry - cfg config.Config + cfg config.LogAreaConfig client *clientv3.Client regex *regexp.Regexp } type LogAreaHandler struct { logger *logrus.Entry - cfg config.Config + cfg config.LogAreaConfig client *clientv3.Client regex *regexp.Regexp } @@ -58,7 +58,7 @@ func (a *LogAreaApplication) Close() { } // New returns a new LogAreaApplication object/struct. -func New(cfg config.Config, log *logrus.Entry) application.Application { +func New(cfg config.LogAreaConfig, log *logrus.Entry) application.Application { cli, err := clientv3.New(clientv3.Config{ Endpoints: []string{cfg.DatabaseURI()}, DialTimeout: 5 * time.Second, diff --git a/pkg/sse/v1/sse.go b/pkg/sse/v1/sse.go index b9ab450..c9a809c 100644 --- a/pkg/sse/v1/sse.go +++ b/pkg/sse/v1/sse.go @@ -25,7 +25,7 @@ import ( "strconv" "time" - config "github.com/eiffel-community/etos-api/internal/configs/sse" + "github.com/eiffel-community/etos-api/internal/config" "github.com/eiffel-community/etos-api/internal/kubernetes" "github.com/eiffel-community/etos-api/pkg/application" "github.com/eiffel-community/etos-api/pkg/events" @@ -35,14 +35,14 @@ import ( type SSEApplication struct { logger *logrus.Entry - cfg config.Config + cfg config.SSEConfig ctx context.Context cancel context.CancelFunc } type SSEHandler struct { logger *logrus.Entry - cfg config.Config + cfg config.SSEConfig ctx context.Context kube *kubernetes.Kubernetes } @@ -53,7 +53,7 @@ func (a *SSEApplication) Close() { } // New returns a new SSEApplication object/struct -func New(cfg config.Config, log *logrus.Entry, ctx context.Context) application.Application { +func New(cfg config.SSEConfig, log *logrus.Entry, ctx context.Context) application.Application { ctx, cancel := context.WithCancel(ctx) return &SSEApplication{ logger: log, diff --git a/pkg/sse/v1alpha/sse.go b/pkg/sse/v1alpha/sse.go index aaf50b2..5b90c9d 100644 --- a/pkg/sse/v1alpha/sse.go +++ b/pkg/sse/v1alpha/sse.go @@ -27,7 +27,7 @@ import ( "strconv" "time" - config "github.com/eiffel-community/etos-api/internal/configs/sse" + "github.com/eiffel-community/etos-api/internal/config" "github.com/eiffel-community/etos-api/internal/kubernetes" "github.com/eiffel-community/etos-api/pkg/application" "github.com/eiffel-community/etos-api/pkg/events" @@ -37,14 +37,14 @@ import ( type SSEApplication struct { logger *logrus.Entry - cfg config.Config + cfg config.SSEConfig ctx context.Context cancel context.CancelFunc } type SSEHandler struct { logger *logrus.Entry - cfg config.Config + cfg config.SSEConfig ctx context.Context kube *kubernetes.Kubernetes } @@ -55,7 +55,7 @@ func (a *SSEApplication) Close() { } // New returns a new SSEApplication object/struct -func New(cfg config.Config, log *logrus.Entry, ctx context.Context) application.Application { +func New(cfg config.SSEConfig, log *logrus.Entry, ctx context.Context) application.Application { ctx, cancel := context.WithCancel(ctx) return &SSEApplication{ logger: log, diff --git a/pkg/sse/v2alpha/sse.go b/pkg/sse/v2alpha/sse.go new file mode 100644 index 0000000..4c2184f --- /dev/null +++ b/pkg/sse/v2alpha/sse.go @@ -0,0 +1,245 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package sse + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + auth "github.com/eiffel-community/etos-api/internal/authorization" + "github.com/eiffel-community/etos-api/internal/authorization/scope" + "github.com/eiffel-community/etos-api/internal/config" + "github.com/eiffel-community/etos-api/internal/stream" + "github.com/eiffel-community/etos-api/pkg/application" + "github.com/eiffel-community/etos-api/pkg/events" + "github.com/julienschmidt/httprouter" + + "github.com/sirupsen/logrus" +) + +const pingInterval = 15 * time.Second + +type Application struct { + logger *logrus.Entry + cfg config.SSEConfig + ctx context.Context + cancel context.CancelFunc + streamer stream.Streamer + authorizer *auth.Authorizer +} + +type Handler struct { + logger *logrus.Entry + cfg config.SSEConfig + ctx context.Context + streamer stream.Streamer +} + +// Close cancels the application context. +func (a *Application) Close() { + a.cancel() + a.streamer.Close() +} + +// New returns a new Application object/struct. +func New(ctx context.Context, cfg config.SSEConfig, log *logrus.Entry, streamer stream.Streamer, authorizer *auth.Authorizer) application.Application { + ctx, cancel := context.WithCancel(ctx) + return &Application{ + logger: log, + cfg: cfg, + ctx: ctx, + cancel: cancel, + streamer: streamer, + authorizer: authorizer, + } +} + +// LoadRoutes loads all the v2alpha routes. +func (a Application) LoadRoutes(router *httprouter.Router) { + handler := &Handler{a.logger, a.cfg, a.ctx, a.streamer} + router.GET("/v2alpha/selftest/ping", handler.Selftest) + router.GET("/v2alpha/events/:identifier", a.authorizer.Middleware(scope.StreamSSE, handler.GetEvents)) + router.POST("/v2alpha/stream/:identifier", a.authorizer.Middleware(scope.DefineSSE, handler.CreateStream)) +} + +// Selftest is a handler to just return 204. +func (h Handler) Selftest(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(http.StatusNoContent) +} + +// cleanFilter will clean up the filters received from clients. +func (h Handler) cleanFilter(identifier string, filters []string) { + for i, filter := range filters { + if len(strings.Split(filter, ".")) != 3 { + filters[i] = fmt.Sprintf("%s.%s", identifier, filter) + } + } +} + +type ErrorEvent struct { + Retry bool `json:"retry"` + Reason string `json:"reason"` +} + +// subscribe subscribes to stream and gets logs and events from it and writes them to a channel. +func (h Handler) subscribe(ctx context.Context, logger *logrus.Entry, streamer stream.Stream, ch chan<- events.Event, counter int, filter []string) { + defer close(ch) + var err error + + consumeCh := make(chan []byte, 0) + + offset := -1 + if counter > 1 { + offset = counter + } + + closed, err := streamer.WithChannel(consumeCh).WithOffset(offset).WithFilter(filter).Consume(ctx) + if err != nil { + logger.WithError(err).Error("failed to start consuming stream") + b, _ := json.Marshal(ErrorEvent{Retry: false, Reason: err.Error()}) + ch <- events.Event{Event: "error", Data: string(b)} + return + } + defer streamer.Close() + + ping := time.NewTicker(pingInterval) + defer ping.Stop() + var event events.Event + for { + select { + case <-ctx.Done(): + logger.Info("Client lost, closing subscriber") + return + case <-ping.C: + ch <- events.Event{Event: "ping"} + case <-closed: + logger.Info("Stream closed, closing down") + b, _ := json.Marshal(ErrorEvent{Retry: true, Reason: "Streamer closed the connection"}) + ch <- events.Event{Event: "error", Data: string(b)} + return + case msg := <-consumeCh: + event, err = events.New(msg) + if err != nil { + logger.WithError(err).Error("failed to parse SSE event") + continue + } + // TODO: https://github.com/eiffel-community/etos/issues/299 + if event.JSONData == nil { + event = events.Event{ + Data: string(msg), + Event: "message", + } + } + event.ID = counter + ch <- event + counter++ + } + } +} + +// CreateStream creates a new RabbitMQ stream for use with the sse events stream. +func (h Handler) CreateStream(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + identifier := ps.ByName("identifier") + // Making it possible for us to correlate logs to a specific connection + logger := h.logger.WithField("identifier", identifier) + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + + err := h.streamer.CreateStream(r.Context(), logger, identifier) + if err != nil { + logger.WithError(err).Error("failed to create a rabbitmq stream") + w.WriteHeader(http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusCreated) +} + +// GetEvents is an endpoint for streaming events and logs from ETOS. +func (h Handler) GetEvents(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + identifier := ps.ByName("identifier") + // Filters may be passed multiple times (?filter=log.info&filter=log.debug) + // in order to parse multiple values into a slice r.ParseForm() is used. + // The filters are accessible in r.Form["filter"] after r.ParseForm() has been + // called. + r.ParseForm() + + // Making it possible for us to correlate logs to a specific connection + logger := h.logger.WithField("identifier", identifier) + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Transfer-Encoding", "chunked") + + lastID := 1 + lastEventID := r.Header.Get("Last-Event-ID") + if lastEventID != "" { + var err error + lastID, err = strconv.Atoi(lastEventID) + if err != nil { + logger.Error("Last-Event-ID header is not parsable") + } + } + + filter := r.Form["filter"] + h.cleanFilter(identifier, filter) + + streamer, err := h.streamer.NewStream(r.Context(), logger, identifier) + if err != nil { + logger.WithError(err).Error("Could not start a new stream") + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(err.Error())) + return + } + + flusher, ok := w.(http.Flusher) + if !ok { + http.NotFound(w, r) + return + } + logger.Info("Client connected to SSE") + + receiver := make(chan events.Event) // Channel is closed in Subscriber + go h.subscribe(r.Context(), logger, streamer, receiver, lastID, filter) + + for { + select { + case <-r.Context().Done(): + logger.Info("Client gone from SSE") + return + case <-h.ctx.Done(): + logger.Info("Shutting down") + return + case event, ok := <-receiver: + if !ok { + return + } + if err := event.Write(w); err != nil { + logger.Error(err) + continue + } + flusher.Flush() + } + } +} diff --git a/pkg/sse/v2alpha/sse_test.go b/pkg/sse/v2alpha/sse_test.go new file mode 100644 index 0000000..aaed3c3 --- /dev/null +++ b/pkg/sse/v2alpha/sse_test.go @@ -0,0 +1,16 @@ +// Copyright Axis Communications AB. +// +// For a full list of individual contributors, please see the commit history. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package sse diff --git a/test/testconfig/testconfig.go b/test/testconfig/testconfig.go index d1b9d3a..96aad6c 100644 --- a/test/testconfig/testconfig.go +++ b/test/testconfig/testconfig.go @@ -17,7 +17,7 @@ package testconfig import ( - config "github.com/eiffel-community/etos-api/internal/configs/base" + "github.com/eiffel-community/etos-api/internal/config" ) type cfg struct { @@ -55,6 +55,11 @@ func (c *cfg) ServicePort() string { return c.servicePort } +// StripPrefix returns the prefix to strip. Empty string if no prefix. +func (c *cfg) StripPrefix() string { + return "" +} + // LogLevel returns the Log level testconfig parameter func (c *cfg) LogLevel() string { return c.logLevel @@ -74,3 +79,8 @@ func (c *cfg) ETOSNamespace() string { func (c *cfg) DatabaseURI() string { return "etcd-client:2379" } + +// PublicKey returns a public key. +func (c *cfg) PublicKey() ([]byte, error) { + return nil, nil +}