diff --git a/Makefile b/Makefile index 2b96f2f..341b864 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ export DOCKER_REGISTRY ?= registry.nordix.org export DOCKER_NAMESPACE ?= eiffel export DEPLOY ?= etos-sse -PROGRAMS = sse logarea +PROGRAMS = sse logarea iut COMPILEDAEMON = $(GOBIN)/CompileDaemon GIT = git GOLANGCI_LINT = $(GOBIN)/golangci-lint @@ -87,6 +87,10 @@ tidy: check-dirty: $(GIT) diff --exit-code HEAD +.PHONY: gen +gen: + go generate ./... + # Setup the dynamic commands # diff --git a/README.rst b/README.rst index bd09c90..a9652a7 100644 --- a/README.rst +++ b/README.rst @@ -20,6 +20,14 @@ Installation pip install . +Running dockers in development mode +=================================== + + make DEPLOY=etos-iut start + make DEPLOY=etos-logarea start + make DEPLOY=etos-sse start + + Contribute ========== diff --git a/cmd/iut/main.go b/cmd/iut/main.go new file mode 100644 index 0000000..19cb0bd --- /dev/null +++ b/cmd/iut/main.go @@ -0,0 +1,133 @@ +// 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 main + +import ( + "context" + "net/http" + "os" + "os/signal" + "runtime/debug" + "syscall" + "time" + + config "github.com/eiffel-community/etos-api/internal/configs/iut" + "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" + "github.com/eiffel-community/etos-api/pkg/iut/v1alpha1" + "github.com/sirupsen/logrus" + "github.com/snowzach/rotatefilehook" + "go.elastic.co/ecslogrus" + clientv3 "go.etcd.io/etcd/client/v3" +) + +// main sets up logging and starts up the webserver. +func main() { + cfg := config.Get() + 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 IUT Provider Service Mini", + "version": vcsRevision(), + "name": "ETOS IUT Provider Mini", + "user_log": false, + }) + + // Database connection test + cli, err := clientv3.New(clientv3.Config{ + Endpoints: []string{cfg.DatabaseURI()}, + DialTimeout: 5 * time.Second, + }) + if err != nil { + log.WithError(err).Fatal("failed to create etcd connection") + } + + log.Info("Loading v1alpha1 routes") + v1alpha1App := v1alpha1.New(cfg, log, ctx, cli) + defer v1alpha1App.Close() + router := application.New(v1alpha1App) + + srv := server.NewWebService(cfg, log, router) + + done := make(chan os.Signal, 1) + signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + if err := srv.Start(); err != nil && err != http.ErrServerClosed { + log.Errorf("Webserver shutdown: %+v", err) + } + }() + + <-done + log.Info("SIGTERM received") + + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + v1alpha1App.Close() + + if err := srv.Close(ctx); err != nil { + log.Errorf("Webserver shutdown failed: %+v", err) + } +} + +// 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 +} + +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 b577f70..7ce6c3e 100644 --- a/cmd/logarea/main.go +++ b/cmd/logarea/main.go @@ -24,7 +24,7 @@ import ( "syscall" "time" - "github.com/eiffel-community/etos-api/internal/config" + config "github.com/eiffel-community/etos-api/internal/configs/logarea" "github.com/eiffel-community/etos-api/internal/logging" "github.com/eiffel-community/etos-api/internal/server" "github.com/eiffel-community/etos-api/pkg/application" diff --git a/cmd/sse/main.go b/cmd/sse/main.go index 3fdc0cc..c18b87d 100644 --- a/cmd/sse/main.go +++ b/cmd/sse/main.go @@ -24,7 +24,7 @@ import ( "syscall" "time" - "github.com/eiffel-community/etos-api/internal/config" + config "github.com/eiffel-community/etos-api/internal/configs/sse" "github.com/eiffel-community/etos-api/internal/logging" "github.com/eiffel-community/etos-api/internal/server" "github.com/eiffel-community/etos-api/pkg/application" diff --git a/deploy/etos-iut/Dockerfile b/deploy/etos-iut/Dockerfile new file mode 100644 index 0000000..f5d47fe --- /dev/null +++ b/deploy/etos-iut/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.21-alpine AS build +WORKDIR /tmp/iut +COPY . . +RUN apk add --no-cache make=4.4.1-r2 git=2.45.2-r0 && make iut + +FROM alpine:3.17.3 +ARG TZ +ENV TZ=$TZ + +LABEL org.opencontainers.image.source=https://github.com/eiffel-community/etos-api +LABEL org.opencontainers.image.authors=etos-maintainers@googlegroups.com +LABEL org.opencontainers.image.licenses=Apache-2.0 + +RUN apk add --no-cache tzdata=2024a-r0 +ENTRYPOINT ["/app/iut"] + +COPY --from=build /tmp/iut/bin/iut /app/iut diff --git a/deploy/etos-iut/Dockerfile.dev b/deploy/etos-iut/Dockerfile.dev new file mode 100644 index 0000000..99fd779 --- /dev/null +++ b/deploy/etos-iut/Dockerfile.dev @@ -0,0 +1,8 @@ +FROM golang:1.21 +WORKDIR /app + +COPY ./go.mod ./go.sum ./ +RUN go mod tidy +COPY . . +RUN git config --global --add safe.directory /app +EXPOSE 8080 diff --git a/deploy/etos-iut/docker-compose.yml b/deploy/etos-iut/docker-compose.yml new file mode 100644 index 0000000..e525fc7 --- /dev/null +++ b/deploy/etos-iut/docker-compose.yml @@ -0,0 +1,16 @@ +version: "3.7" +services: + etos-iut: + build: + context: . + dockerfile: ./deploy/etos-iut/Dockerfile.dev + args: + http_proxy: "${http_proxy}" + https_proxy: "${https_proxy}" + volumes: + - ./:/app + ports: + - 8080:8080 + env_file: + - ./configs/development.env + entrypoint: ["/app/bin/iut"] diff --git a/deploy/etos-logarea/docker-compose.yml b/deploy/etos-logarea/docker-compose.yml index 8a4058e..64c10c6 100644 --- a/deploy/etos-logarea/docker-compose.yml +++ b/deploy/etos-logarea/docker-compose.yml @@ -13,4 +13,4 @@ services: - 8080:8080 env_file: - ./configs/development.env - entrypoint: ["/bin/bash", "./scripts/entrypoint.sh"] + entrypoint: ["/app/bin/logarea"] diff --git a/go.mod b/go.mod index e5d97c9..0c16e4d 100644 --- a/go.mod +++ b/go.mod @@ -5,24 +5,33 @@ go 1.21 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/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/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.8.4 + github.com/stretchr/testify v1.9.0 go.elastic.co/ecslogrus v1.0.0 - go.etcd.io/etcd/client/v3 v3.5.14 + 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 k8s.io/apimachinery v0.28.2 k8s.io/client-go v0.28.2 ) require ( + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Showmax/go-fqdn v1.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/clarketm/json v1.17.1 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -40,7 +49,6 @@ require ( github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.3.1 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect @@ -51,10 +59,12 @@ require ( github.com/json-iterator/go v1.1.12 // 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/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.11.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect @@ -62,11 +72,13 @@ require ( github.com/prometheus/procfs v0.6.0 // indirect github.com/soheilhy/cmux v0.1.5 // 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 + github.com/tidwall/pretty v1.2.0 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // 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.14 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.14 // 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 @@ -86,7 +98,7 @@ require ( golang.org/x/oauth2 v0.11.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/term v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/text v0.15.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 diff --git a/go.sum b/go.sum index 22aa92c..7b297b3 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,10 @@ cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdi cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= 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= +github.com/Showmax/go-fqdn v1.0.0 h1:0rG5IbmVliNT5O19Mfuvna9LL7zlHyRfsSvBPZmF9tM= +github.com/Showmax/go-fqdn v1.0.0/go.mod h1:SfrFBzmDCtCGrnHhoDjuvFnKsWjEQX/Q9ARZvOrJAko= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -22,6 +26,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clarketm/json v1.17.1 h1:U1IxjqJkJ7bRK4L6dyphmoO840P6bdhPdbbLySourqI= +github.com/clarketm/json v1.17.1/go.mod h1:ynr2LRfb0fQU34l07csRNBTcivjySLLiY1YzQqKVfdo= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= @@ -38,6 +44,8 @@ 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/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -111,8 +119,8 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ 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/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/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/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= @@ -153,10 +161,14 @@ 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= @@ -177,6 +189,8 @@ github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLyw 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/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/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= @@ -205,6 +219,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 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/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= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -229,8 +245,14 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= @@ -241,14 +263,14 @@ go.elastic.co/ecslogrus v1.0.0 h1:o1qvcCNaq+eyH804AuK6OOiUupLIXVDfYjDtSLPwukM= go.elastic.co/ecslogrus v1.0.0/go.mod h1:vMdpljurPbwu+iFmNc/HSWCkn1Fu/dYde1o/adaEczo= go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= -go.etcd.io/etcd/api/v3 v3.5.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0= -go.etcd.io/etcd/api/v3 v3.5.14/go.mod h1:BmtWcRlQvwa1h3G2jvKYwIQy4PkHlDej5t7uLMUdJUU= -go.etcd.io/etcd/client/pkg/v3 v3.5.14 h1:SaNH6Y+rVEdxfpA2Jr5wkEvN6Zykme5+YnbCkxvuWxQ= -go.etcd.io/etcd/client/pkg/v3 v3.5.14/go.mod h1:8uMgAokyG1czCtIdsq+AGyYQMvpIKnSvPjFMunkgeZI= +go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk= +go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM= +go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA= +go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU= go.etcd.io/etcd/client/v2 v2.305.14 h1:v5ASLyFuMlVd/gKU6uf6Cod+vSWKa4Rsv9+eghl0Nwk= go.etcd.io/etcd/client/v2 v2.305.14/go.mod h1:AWYT0lLEkBuqVaGw0UVMtA4rxCb3/oGE8PxZ8cUS4tI= -go.etcd.io/etcd/client/v3 v3.5.14 h1:CWfRs4FDaDoSz81giL7zPpZH2Z35tbOrAJkkjMqOupg= -go.etcd.io/etcd/client/v3 v3.5.14/go.mod h1:k3XfdV/VIHy/97rqWjoUzrj9tk7GgJGH9J8L4dNXmAk= +go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4= +go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU= go.etcd.io/etcd/pkg/v3 v3.5.14 h1:keuxhJiDCPjTKpW77GxJnnVVD5n4IsfvkDaqiqUMNEQ= go.etcd.io/etcd/pkg/v3 v3.5.14/go.mod h1:7o+DL6a7DYz9KSjWByX+NGmQPYinoH3D36VAu/B3JqA= go.etcd.io/etcd/raft/v3 v3.5.14 h1:mHnpbljpBBftmK+YUfp+49ivaCc126aBPLAnwDw0DnE= @@ -347,8 +369,8 @@ golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/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= diff --git a/internal/config/config.go b/internal/configs/base/config.go similarity index 100% rename from internal/config/config.go rename to internal/configs/base/config.go diff --git a/internal/config/config_test.go b/internal/configs/base/config_test.go similarity index 100% rename from internal/config/config_test.go rename to internal/configs/base/config_test.go diff --git a/internal/configs/iut/config.go b/internal/configs/iut/config.go new file mode 100644 index 0000000..4b8fb0b --- /dev/null +++ b/internal/configs/iut/config.go @@ -0,0 +1,96 @@ +// 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 new file mode 100644 index 0000000..b5039da --- /dev/null +++ b/internal/configs/iut/config_test.go @@ -0,0 +1,81 @@ +// 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 new file mode 100644 index 0000000..36c5f67 --- /dev/null +++ b/internal/configs/logarea/config.go @@ -0,0 +1,107 @@ +// 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 new file mode 100644 index 0000000..abb00ee --- /dev/null +++ b/internal/configs/logarea/config_test.go @@ -0,0 +1,71 @@ +// 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 new file mode 100644 index 0000000..36c5f67 --- /dev/null +++ b/internal/configs/sse/config.go @@ -0,0 +1,107 @@ +// 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/sse/config_test.go b/internal/configs/sse/config_test.go new file mode 100644 index 0000000..abb00ee --- /dev/null +++ b/internal/configs/sse/config_test.go @@ -0,0 +1,71 @@ +// 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/iut/server/server.go b/internal/iut/server/server.go new file mode 100644 index 0000000..3772638 --- /dev/null +++ b/internal/iut/server/server.go @@ -0,0 +1,63 @@ +// 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 server + +import ( + "context" + "fmt" + "net/http" + + config "github.com/eiffel-community/etos-api/internal/configs/iut" + "github.com/sirupsen/logrus" +) + +// Server interface for serving up the Provider Service. +type Server interface { + Start() error + Close(ctx context.Context) error +} + +// Webserver is a struct for webservers implementing the Server interface. +type WebServer struct { + server *http.Server + cfg config.Config + logger *logrus.Entry +} + +// NewWebserver creates a new Server of the webserver type. +func NewWebserver(cfg config.Config, log *logrus.Entry, handler http.Handler) Server { + webserver := &WebServer{ + server: &http.Server{ + Addr: fmt.Sprintf("%s:%s", cfg.ServiceHost(), cfg.ServicePort()), + Handler: handler, + }, + cfg: cfg, + logger: log, + } + return webserver +} + +// Start a webserver and block until closed or crashed. +func (s *WebServer) Start() error { + s.logger.Infof("Starting webserver listening on %s:%s", s.cfg.ServiceHost(), s.cfg.ServicePort()) + return s.server.ListenAndServe() +} + +// Close calls shutdown on the webserver. Shutdown times out if context is cancelled. +func (s *WebServer) Close(ctx context.Context) error { + s.logger.Info("Shutting down webserver") + return s.server.Shutdown(ctx) +} diff --git a/internal/kubernetes/kubernetes.go b/internal/kubernetes/kubernetes.go index a43a955..cb8bee7 100644 --- a/internal/kubernetes/kubernetes.go +++ b/internal/kubernetes/kubernetes.go @@ -19,7 +19,7 @@ import ( "context" "fmt" - "github.com/eiffel-community/etos-api/internal/config" + config "github.com/eiffel-community/etos-api/internal/configs/base" "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 6529e53..79fd9f3 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -20,7 +20,7 @@ import ( "fmt" "net/http" - "github.com/eiffel-community/etos-api/internal/config" + config "github.com/eiffel-community/etos-api/internal/configs/base" "github.com/sirupsen/logrus" ) diff --git a/manifests/base/iut/deployment.yaml b/manifests/base/iut/deployment.yaml new file mode 100644 index 0000000..652f5ba --- /dev/null +++ b/manifests/base/iut/deployment.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/name: etos-api + app.kubernetes.io/part-of: etos + app.kubernetes.io/component: iut + name: etos-iut +spec: + selector: + matchLabels: + app.kubernetes.io/name: etos-api + app.kubernetes.io/component: iut + template: + metadata: + labels: + app.kubernetes.io/name: etos-api + app.kubernetes.io/component: iut + spec: + serviceAccountName: etos-iut + containers: + - name: etos-iut + image: registry.nordix.org/eiffel/etos-iut:672f982e + imagePullPolicy: IfNotPresent + env: + - name: SERVICE_HOST + value: 0.0.0.0 + ports: + - name: http + containerPort: 8080 + protocol: TCP + livenessProbe: + httpGet: + path: /v1alpha1/selftest/ping + port: http + readinessProbe: + httpGet: + path: /v1alpha1/selftest/ping + port: http diff --git a/manifests/base/iut/kustomization.yaml b/manifests/base/iut/kustomization.yaml new file mode 100644 index 0000000..581fbe2 --- /dev/null +++ b/manifests/base/iut/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - service-account.yaml + - service.yaml + - deployment.yaml diff --git a/manifests/base/iut/service-account.yaml b/manifests/base/iut/service-account.yaml new file mode 100644 index 0000000..9208316 --- /dev/null +++ b/manifests/base/iut/service-account.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: etos-api + app.kubernetes.io/part-of: etos + app.kubernetes.io/component: iut + name: etos-iut diff --git a/manifests/base/iut/service.yaml b/manifests/base/iut/service.yaml new file mode 100644 index 0000000..04afeb8 --- /dev/null +++ b/manifests/base/iut/service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: etos-api + app.kubernetes.io/part-of: etos + app.kubernetes.io/component: iut + name: etos-iut +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/name: etos-api + app.kubernetes.io/component: iut + type: ClusterIP diff --git a/manifests/base/kustomization.yaml b/manifests/base/kustomization.yaml index 7aa5492..dbaa3bd 100644 --- a/manifests/base/kustomization.yaml +++ b/manifests/base/kustomization.yaml @@ -9,6 +9,7 @@ resources: - deployment.yaml - ./sse - ./logarea + - ./iut # By generating the configmap it will get a unique name on each apply diff --git a/pkg/iut/v1alpha1/v1alpha1.go b/pkg/iut/v1alpha1/v1alpha1.go new file mode 100644 index 0000000..53f356c --- /dev/null +++ b/pkg/iut/v1alpha1/v1alpha1.go @@ -0,0 +1,261 @@ +// 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 v1alpha1 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "runtime" + "sync" + "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/pkg/application" + packageurl "github.com/package-url/packageurl-go" + clientv3 "go.etcd.io/etcd/client/v3" + + "github.com/google/uuid" + "github.com/julienschmidt/httprouter" + "github.com/sirupsen/logrus" +) + +type V1Alpha1Application struct { + logger *logrus.Entry + cfg config.Config + database *clientv3.Client + wg *sync.WaitGroup +} + +type V1Alpha1Handler struct { + logger *logrus.Entry + cfg config.Config + database *clientv3.Client + wg *sync.WaitGroup +} + +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{}) { + response, _ := json.Marshal(payload) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _, _ = w.Write(response) +} + +// RespondWithError writes a JSON response with an error message and status code to the HTTP ResponseWriter. +func RespondWithError(w http.ResponseWriter, code int, message string) { + RespondWithJSON(w, code, map[string]string{"error": message}) +} + +// Close does nothing atm. Present for interface coherence +func (a *V1Alpha1Application) Close() { + a.wg.Wait() +} + +// New returns a new V1Alpha1Application object/struct +func New(cfg config.Config, log *logrus.Entry, ctx context.Context, cli *clientv3.Client) application.Application { + return &V1Alpha1Application{ + logger: log, + cfg: cfg, + database: cli, + wg: &sync.WaitGroup{}, + } +} + +// LoadRoutes loads all the v1alpha1 routes. +func (a V1Alpha1Application) LoadRoutes(router *httprouter.Router) { + handler := &V1Alpha1Handler{a.logger, a.cfg, a.database, a.wg} + router.GET("/v1alpha1/selftest/ping", handler.Selftest) + router.POST("/start", handler.panicRecovery(handler.timeoutHandler(handler.Start))) + router.GET("/status", handler.panicRecovery(handler.timeoutHandler(handler.Status))) + router.POST("/stop", handler.panicRecovery(handler.timeoutHandler(handler.Stop))) +} + +// Selftest is a handler to just return 204. +func (h V1Alpha1Handler) Selftest(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + RespondWithError(w, http.StatusNoContent, "") +} + +type StartRequest struct { + MinimumAmount int `json:"minimum_amount"` + MaximumAmount int `json:"maximum_amount"` + ArtifactIdentity string `json:"identity"` + ArtifactID string `json:"artifact_id"` + ArtifactCreated eiffelevents.ArtifactCreatedV3 `json:"artifact_created,omitempty"` + ArtifactPublished eiffelevents.ArtifactPublishedV3 `json:"artifact_published,omitempty"` + TERCC eiffelevents.TestExecutionRecipeCollectionCreatedV4 `json:"tercc,omitempty"` + Context uuid.UUID `json:"context,omitempty"` + Dataset Dataset `json:"dataset,omitempty"` +} + +type StartResponse struct { + Id uuid.UUID `json:"id"` +} + +type StatusResponse struct { + Id uuid.UUID `json:"id"` + Status string `json:"status"` + Iuts []packageurl.PackageURL `json:"iuts"` +} + +type StatusRequest struct { + Id uuid.UUID `json:"id"` +} + +type StopRequest struct { + Id uuid.UUID `json:"id"` +} + +// Start creates a number of IUTs and stores them in the ETCD database returning a checkout ID. +func (h V1Alpha1Handler) Start(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + checkOutID := uuid.New() + + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Content-Type", "application/json") + + var startReq StartRequest + if err := json.NewDecoder(r.Body).Decode(&startReq); err != nil { + RespondWithError(w, http.StatusBadRequest, err.Error()) + return + } + defer r.Body.Close() + purl, err := packageurl.FromString(startReq.ArtifactIdentity) + if err != nil { + RespondWithError(w, http.StatusBadRequest, err.Error()) + return + } + + purls := make([]packageurl.PackageURL, startReq.MinimumAmount) + for i := range purls { + purls[i] = purl + } + iuts, err := json.Marshal(purls) + if err != nil { + RespondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + _, err = h.database.Put(r.Context(), fmt.Sprintf("/iut/%s", checkOutID.String()), string(iuts)) + if err != nil { + RespondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + startResp := StartResponse{Id: checkOutID} + w.WriteHeader(http.StatusOK) + response, _ := json.Marshal(startResp) + _, _ = w.Write(response) +} + +// Status creates a simple DONE Status response with IUTs. +func (h V1Alpha1Handler) Status(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + identifier := r.Header.Get("X-Etos-Id") + logger := h.logger.WithField("identifier", identifier).WithContext(r.Context()) + + id, err := uuid.Parse(r.URL.Query().Get("id")) + + key := fmt.Sprintf("/iut/%s", id) + dbResp, err := h.database.Get(r.Context(), key) + if err != nil { + logger.Errorf("Failed to look up status request id: %s", id) + RespondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + if len(dbResp.Kvs) == 0 { + err = fmt.Errorf("No key found: %s", key) + RespondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + statusResp := StatusResponse{ + Id: id, + Status: "DONE", + } + if err = json.Unmarshal(dbResp.Kvs[0].Value, &statusResp.Iuts); err != nil { + RespondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + response, err := json.Marshal(statusResp) + if err != nil { + RespondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write(response) +} + +// Stop deletes the given IUTs from the database and returns an empty response. +func (h V1Alpha1Handler) Stop(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + identifier := r.Header.Get("X-Etos-Id") + logger := h.logger.WithField("identifier", identifier).WithContext(r.Context()) + + var stopReq StopRequest + defer r.Body.Close() + if err := json.NewDecoder(r.Body).Decode(&stopReq); err != nil { + logger.Errorf("Bad delete request: %s", err.Error()) + RespondWithError(w, http.StatusBadRequest, err.Error()) + return + } + _, err := h.database.Delete(r.Context(), fmt.Sprintf("/iut/%s", stopReq.Id)) + if err != nil { + logger.Errorf("Etcd delete failed: %s", err.Error()) + RespondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + w.WriteHeader(http.StatusNoContent) +} + +// timeoutHandler will change the request context to a timeout context. +func (h V1Alpha1Handler) timeoutHandler( + 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) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + newRequest := r.WithContext(ctx) + fn(w, newRequest, ps) + } +} + +// panicRecovery tracks panics from the service, logs them and returns an error response to the user. +func (h V1Alpha1Handler) panicRecovery( + 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) { + defer func() { + if err := recover(); err != nil { + buf := make([]byte, 2048) + n := runtime.Stack(buf, false) + buf = buf[:n] + h.logger.WithField( + "identifier", ps.ByName("identifier"), + ).WithContext( + r.Context(), + ).Errorf("recovering from err %+v\n %s", err, buf) + identifier := ps.ByName("identifier") + RespondWithError( + w, + http.StatusInternalServerError, + fmt.Sprintf("unknown error: contact server admin with id '%s'", identifier), + ) + } + }() + fn(w, r, ps) + } +} diff --git a/pkg/logarea/v1alpha/logarea.go b/pkg/logarea/v1alpha/logarea.go index 63ed96b..3b016c3 100644 --- a/pkg/logarea/v1alpha/logarea.go +++ b/pkg/logarea/v1alpha/logarea.go @@ -25,7 +25,7 @@ import ( "runtime" "time" - "github.com/eiffel-community/etos-api/internal/config" + config "github.com/eiffel-community/etos-api/internal/configs/logarea" "github.com/eiffel-community/etos-api/pkg/application" "github.com/julienschmidt/httprouter" "github.com/sirupsen/logrus" diff --git a/pkg/sse/v1/sse.go b/pkg/sse/v1/sse.go index 194fe6d..5b7ec20 100644 --- a/pkg/sse/v1/sse.go +++ b/pkg/sse/v1/sse.go @@ -25,7 +25,7 @@ import ( "strconv" "time" - "github.com/eiffel-community/etos-api/internal/config" + config "github.com/eiffel-community/etos-api/internal/configs/sse" "github.com/eiffel-community/etos-api/internal/kubernetes" "github.com/eiffel-community/etos-api/pkg/application" "github.com/eiffel-community/etos-api/pkg/events" diff --git a/pkg/sse/v1alpha/sse.go b/pkg/sse/v1alpha/sse.go index 3317511..aaf50b2 100644 --- a/pkg/sse/v1alpha/sse.go +++ b/pkg/sse/v1alpha/sse.go @@ -27,7 +27,7 @@ import ( "strconv" "time" - "github.com/eiffel-community/etos-api/internal/config" + config "github.com/eiffel-community/etos-api/internal/configs/sse" "github.com/eiffel-community/etos-api/internal/kubernetes" "github.com/eiffel-community/etos-api/pkg/application" "github.com/eiffel-community/etos-api/pkg/events" diff --git a/test/testconfig/testconfig.go b/test/testconfig/testconfig.go index df5d9fe..d1b9d3a 100644 --- a/test/testconfig/testconfig.go +++ b/test/testconfig/testconfig.go @@ -17,7 +17,7 @@ package testconfig import ( - "github.com/eiffel-community/etos-api/internal/config" + config "github.com/eiffel-community/etos-api/internal/configs/base" ) type cfg struct {