From 20a054a206638fd183abb0d2b97c6c41a9a016ff Mon Sep 17 00:00:00 2001 From: Andrei Matveyeu Date: Thu, 29 Aug 2024 12:58:00 +0200 Subject: [PATCH 1/6] Generic IUT provider --- Makefile | 2 +- cmd/iut/main.go | 139 +++++++++ cmd/logarea/main.go | 2 +- cmd/sse/main.go | 2 +- deploy/etos-iut/Dockerfile | 17 ++ deploy/etos-iut/Dockerfile.dev | 8 + deploy/etos-iut/docker-compose.yml | 16 ++ go.mod | 24 +- go.sum | 46 ++- internal/{config => configs/base}/config.go | 0 .../{config => configs/base}/config_test.go | 0 internal/configs/iut/config.go | 139 +++++++++ internal/configs/iut/config_test.go | 98 +++++++ internal/configs/logarea/config.go | 107 +++++++ internal/configs/logarea/config_test.go | 71 +++++ internal/configs/sse/config.go | 107 +++++++ internal/configs/sse/config_test.go | 71 +++++ internal/iut/contextmanager/contextmanager.go | 84 ++++++ internal/iut/responses/responses.go | 35 +++ internal/iut/responses/responses_test.go | 42 +++ internal/iut/server/server.go | 63 +++++ internal/kubernetes/kubernetes.go | 2 +- internal/server/server.go | 2 +- manifests/base/iut/deployment.yaml | 39 +++ manifests/base/iut/kustomization.yaml | 6 + manifests/base/iut/service-account.yaml | 8 + manifests/base/iut/service.yaml | 18 ++ manifests/base/kustomization.yaml | 1 + pkg/iut/application/application.go | 35 +++ pkg/iut/application/application_test.go | 83 ++++++ pkg/iut/v1alpha1/v1alpha1.go | 263 ++++++++++++++++++ pkg/logarea/v1alpha/logarea.go | 2 +- pkg/sse/v1/sse.go | 2 +- pkg/sse/v1alpha/sse.go | 2 +- test/testconfig/testconfig.go | 2 +- 35 files changed, 1511 insertions(+), 27 deletions(-) create mode 100644 cmd/iut/main.go create mode 100644 deploy/etos-iut/Dockerfile create mode 100644 deploy/etos-iut/Dockerfile.dev create mode 100644 deploy/etos-iut/docker-compose.yml rename internal/{config => configs/base}/config.go (100%) rename internal/{config => configs/base}/config_test.go (100%) create mode 100644 internal/configs/iut/config.go create mode 100644 internal/configs/iut/config_test.go create mode 100644 internal/configs/logarea/config.go create mode 100644 internal/configs/logarea/config_test.go create mode 100644 internal/configs/sse/config.go create mode 100644 internal/configs/sse/config_test.go create mode 100644 internal/iut/contextmanager/contextmanager.go create mode 100644 internal/iut/responses/responses.go create mode 100644 internal/iut/responses/responses_test.go create mode 100644 internal/iut/server/server.go create mode 100644 manifests/base/iut/deployment.yaml create mode 100644 manifests/base/iut/kustomization.yaml create mode 100644 manifests/base/iut/service-account.yaml create mode 100644 manifests/base/iut/service.yaml create mode 100644 pkg/iut/application/application.go create mode 100644 pkg/iut/application/application_test.go create mode 100644 pkg/iut/v1alpha1/v1alpha1.go diff --git a/Makefile b/Makefile index 2b96f2f..32d1d24 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 diff --git a/cmd/iut/main.go b/cmd/iut/main.go new file mode 100644 index 0000000..424437a --- /dev/null +++ b/cmd/iut/main.go @@ -0,0 +1,139 @@ +// 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/iut/contextmanager" + server "github.com/eiffel-community/etos-api/internal/iut/server" + "github.com/eiffel-community/etos-api/internal/logging" + "github.com/eiffel-community/etos-api/pkg/iut/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": "Dummy IUT Provider Service", + "version": vcsRevision(), + "name": "Dummy IUT Provider", + "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") + } + + cm := contextmanager.New(cli) + go cm.Start(ctx) + defer cm.CancelAll() + + log.Info("Loading v1alpha1 routes") + v1alpha1App := v1alpha1.New(cfg, log, ctx, cm, cli) + defer v1alpha1App.Close() + router := application.New(v1alpha1App) + + srv := server.NewWebserver(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, cfg.Timeout()) + defer cancel() + v1alpha1App.Close() + + if err := srv.Close(ctx); err != nil { + log.Errorf("Webserver shutdown failed: %+v", err) + } + log.Info("Wait for checkout, deploy, status and checkin jobs 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 +} + +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..4128058 --- /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: ["/bin/bash", "./scripts/entrypoint.sh"] 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..5fb99ab --- /dev/null +++ b/internal/configs/iut/config.go @@ -0,0 +1,139 @@ +// 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" + "time" + + "github.com/sirupsen/logrus" +) + +// Config interface for retrieving configuration options. +type Config interface { + ServiceHost() string + ServicePort() string + LogLevel() string + LogFilePath() string + Timeout() time.Duration + EventRepositoryHost() string + IutWaitTimeoutHard() time.Duration + IutWaitTimeoutSoft() time.Duration + DatabaseURI() string +} + +// cfg implements the Config interface. +type cfg struct { + serviceHost string + servicePort string + logLevel string + logFilePath string + timeout time.Duration + databaseHost string + databasePort string + eventRepositoryHost string + iutWaitTimeoutHard time.Duration + iutWaitTimeoutSoft time.Duration +} + +// 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) + } + + iutWaitTimeoutHard, err := time.ParseDuration(EnvOrDefault("IUT_WAIT_TIMEOUT", "1h")) + if err != nil { + logrus.Panic(err) + } + + iutWaitTimeoutSoft, err := time.ParseDuration(EnvOrDefault("IUT_WAIT_TIMEOUT_SOFT", "30m")) + 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.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 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.eventRepositoryHost, "eventrepository_url", os.Getenv("ETOS_GRAPHQL_SERVER"), "URL to the GraphQL server to use for event lookup.") + flag.DurationVar(&conf.iutWaitTimeoutHard, "hard iut wait timeout", iutWaitTimeoutHard, "Hard wait timeout for IUT checkout") + flag.DurationVar(&conf.iutWaitTimeoutSoft, "soft iut wait timeout", iutWaitTimeoutSoft, "Soft wait timeout for IUT checkout") + 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 +} + +// Timeout returns the request timeout for Provider Service API. +func (c *cfg) Timeout() time.Duration { + return c.timeout +} + +// EventRepositoryHost returns the host to use for event lookups. +func (c *cfg) EventRepositoryHost() string { + return c.eventRepositoryHost +} + +// IutWaitTimeoutHard returns the hard timeout for IUT checkout +func (c *cfg) IutWaitTimeoutHard() time.Duration { + return c.iutWaitTimeoutHard +} + +// IutWaitTimeoutSoft returns the soft timeout for IUT checkout +func (c *cfg) IutWaitTimeoutSoft() time.Duration { + return c.iutWaitTimeoutSoft +} + +// 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..6ad30ca --- /dev/null +++ b/internal/configs/iut/config_test.go @@ -0,0 +1,98 @@ +// 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" + "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" + 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("REQUEST_TIMEOUT", timeoutStr) + os.Setenv("ETOS_ETCD_HOST", databaseHost) + os.Setenv("ETOS_ETCD_PORT", databasePort) + + 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, databaseHost, conf.databaseHost) + assert.Equal(t, databasePort, conf.databasePort) + assert.Equal(t, timeout, conf.timeout) + 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", + 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()) + }) + } +} + +// 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/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/contextmanager/contextmanager.go b/internal/iut/contextmanager/contextmanager.go new file mode 100644 index 0000000..ce3e09b --- /dev/null +++ b/internal/iut/contextmanager/contextmanager.go @@ -0,0 +1,84 @@ +// 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 contextmanager + +import ( + "context" + "regexp" + "strings" + + "go.etcd.io/etcd/api/v3/mvccpb" + clientv3 "go.etcd.io/etcd/client/v3" +) + +const REGEX = "/testrun/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/provider/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/status" + +// ContextManager manages contexts for the IUT provider goroutines, enabling the cancelation +// of the context in the case where a testrun should be removed during the checkout phase. +type ContextManager struct { + contexts map[string]context.CancelFunc + db *clientv3.Client +} + +// New creates a new context manager. +func New(db *clientv3.Client) *ContextManager { + return &ContextManager{ + contexts: make(map[string]context.CancelFunc), + db: db, + } +} + +// CancelAll cancels all saved contexts within the context manager. +func (c *ContextManager) CancelAll() { + for _, cancel := range c.contexts { + cancel() + } + c.contexts = make(map[string]context.CancelFunc) +} + +// Start up the context manager testrun deletaion watcher. +func (c *ContextManager) Start(ctx context.Context) { + regex := regexp.MustCompile(REGEX) + ch := c.db.Watch(ctx, "/testrun", clientv3.WithPrefix()) + for response := range ch { + for _, event := range response.Events { + if event.Type == mvccpb.DELETE { + if !regex.Match(event.Kv.Key) { + continue + } + dbPath := strings.Split(string(event.Kv.Key), "/") + c.Cancel(dbPath[2]) + } + } + } +} + +// Cancel the context for one stored ID. +func (c *ContextManager) Cancel(id string) { + cancel, ok := c.contexts[id] + if !ok { + return + } + delete(c.contexts, id) + cancel() +} + +// Add a new context to the context manager. +func (c *ContextManager) Add(ctx context.Context, id string) context.Context { + ctx, cancel := context.WithCancel(ctx) + c.contexts[id] = cancel + return ctx +} diff --git a/internal/iut/responses/responses.go b/internal/iut/responses/responses.go new file mode 100644 index 0000000..458ef23 --- /dev/null +++ b/internal/iut/responses/responses.go @@ -0,0 +1,35 @@ +// 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 responses + +import ( + "encoding/json" + "net/http" +) + +// 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}) +} diff --git a/internal/iut/responses/responses_test.go b/internal/iut/responses/responses_test.go new file mode 100644 index 0000000..a819deb --- /dev/null +++ b/internal/iut/responses/responses_test.go @@ -0,0 +1,42 @@ +// 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 responses + +import ( + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Test that RespondWithJSON writes the correct HTTP code, message and adds a content type header. +func TestRespondWithJSON(t *testing.T) { + responseRecorder := httptest.NewRecorder() + RespondWithJSON(responseRecorder, 200, map[string]string{"hello": "world"}) + assert.Equal(t, "application/json", responseRecorder.Header().Get("Content-Type")) + assert.Equal(t, 200, responseRecorder.Result().StatusCode) + assert.JSONEq(t, `{"hello": "world"}`, responseRecorder.Body.String()) +} + +// Test that RespondWithError writes the correct HTTP code, message and adds a content type header. +func TestRespondWithError(t *testing.T) { + responseRecorder := httptest.NewRecorder() + RespondWithError(responseRecorder, 400, "failure") + assert.Equal(t, "application/json", responseRecorder.Header().Get("Content-Type")) + assert.Equal(t, 400, responseRecorder.Result().StatusCode) + assert.JSONEq(t, `{"error": "failure"}`, responseRecorder.Body.String()) +} 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/application/application.go b/pkg/iut/application/application.go new file mode 100644 index 0000000..cb4fc0d --- /dev/null +++ b/pkg/iut/application/application.go @@ -0,0 +1,35 @@ +// 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 application + +import "github.com/julienschmidt/httprouter" + +type Application interface { + LoadRoutes(*httprouter.Router) + Close() +} + +// New loads routes for all applications supplied and returns a new router to +// be used in the server. +func New(application Application, applications ...Application) *httprouter.Router { + router := httprouter.New() + application.LoadRoutes(router) + for _, app := range applications { + app.LoadRoutes(router) + } + return router +} diff --git a/pkg/iut/application/application_test.go b/pkg/iut/application/application_test.go new file mode 100644 index 0000000..0e5d682 --- /dev/null +++ b/pkg/iut/application/application_test.go @@ -0,0 +1,83 @@ +// 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 application + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" +) + +type testApp struct { + route string + message string +} + +// testRoute is a test route that prints a test message from the app to which it is "attached". +func (t *testApp) testRoute(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + fmt.Fprint(w, t.message) +} + +// LoadRoutes from the application to which it is "attached". +func (t *testApp) LoadRoutes(router *httprouter.Router) { + router.GET(t.route, t.testRoute) +} + +// Close is a placeholder to fulfill implementation of the application interface. +func (t *testApp) Close() {} + +// TestNew verifies that it is possible to load a handlers routes. +func TestNew(t *testing.T) { + app := &testApp{ + route: "/testing", + message: "hello", + } + router := New(app) + + responseRecorder := httptest.NewRecorder() + request := httptest.NewRequest("GET", app.route, nil) + router.ServeHTTP(responseRecorder, request) + assert.Equal(t, 200, responseRecorder.Code) + assert.Equal(t, app.message, responseRecorder.Body.String()) +} + +// TestNew verifies that it is possible to load multiple handlers routes. +func TestNewMultiple(t *testing.T) { + route1 := &testApp{"/route1", "hello1"} + route2 := &testApp{"/route2", "hello2"} + tests := []struct { + name string + app *testApp + }{ + {name: "Route1", app: route1}, + {name: "Route1", app: route2}, + } + + router := New(route1, route2) + + for _, testCase := range tests { + responseRecorder := httptest.NewRecorder() + request := httptest.NewRequest("GET", testCase.app.route, nil) + router.ServeHTTP(responseRecorder, request) + assert.Equal(t, 200, responseRecorder.Code) + assert.Equal(t, testCase.app.message, responseRecorder.Body.String()) + } +} diff --git a/pkg/iut/v1alpha1/v1alpha1.go b/pkg/iut/v1alpha1/v1alpha1.go new file mode 100644 index 0000000..8dc1fc8 --- /dev/null +++ b/pkg/iut/v1alpha1/v1alpha1.go @@ -0,0 +1,263 @@ +// 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" + + 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/iut/contextmanager" + "github.com/eiffel-community/etos-api/internal/iut/responses" + "github.com/eiffel-community/etos-api/pkg/iut/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" +) + +var ( + service_version string +) + +// BASEREGEX for matching /testrun/tercc-id/provider/iuts/reference. +const BASEREGEX = "/testrun/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/provider/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/iuts" + +const EtcdTreePathPrefix = "/purl" + +type V1Alpha1Application struct { + logger *logrus.Entry + cfg config.Config + database *clientv3.Client + cm *contextmanager.ContextManager + wg *sync.WaitGroup +} + +type V1Alpha1Handler struct { + logger *logrus.Entry + cfg config.Config + database *clientv3.Client + cm *contextmanager.ContextManager + wg *sync.WaitGroup +} + +type Dataset struct { + Greed interface{} `json:"greed"` +} + +// 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, cm *contextmanager.ContextManager, cli *clientv3.Client) application.Application { + return &V1Alpha1Application{ + logger: log, + cfg: cfg, + database: cli, + cm: cm, + 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.cm, 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) { + responses.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 StartResponse with the required number of IUTs (package URLs) +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 { + responses.RespondWithError(w, http.StatusBadRequest, err.Error()) + return + } + defer r.Body.Close() + purl, err := packageurl.FromString(startReq.ArtifactIdentity) + if err != nil { + responses.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 { + responses.RespondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + _, err = h.database.Put(r.Context(), fmt.Sprintf("/iut/%s", checkOutID.String()), string(iuts)) + if err != nil { + responses.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 to indicate IUTs have been checked out. +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()) + + defer r.Body.Close() + 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) + responses.RespondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + if len(dbResp.Kvs) == 0 { + err = fmt.Errorf("No key found: %s", key) + responses.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 { + responses.RespondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + response, err := json.Marshal(statusResp) + if err != nil { + responses.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()) + responses.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()) + responses.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(), h.cfg.Timeout()) + 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") + responses.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 { From 3451918c7f735920b206822bc75af2c86fa01d64 Mon Sep 17 00:00:00 2001 From: Andrei Matveyeu Date: Thu, 29 Aug 2024 12:58:00 +0200 Subject: [PATCH 2/6] Removed unused code, fixed comments --- cmd/iut/main.go | 6 +- internal/configs/iut/config.go | 7 +++ pkg/iut/application/application.go | 35 ----------- pkg/iut/application/application_test.go | 83 ------------------------- pkg/iut/v1alpha1/v1alpha1.go | 59 +++++++++--------- 5 files changed, 40 insertions(+), 150 deletions(-) delete mode 100644 pkg/iut/application/application.go delete mode 100644 pkg/iut/application/application_test.go diff --git a/cmd/iut/main.go b/cmd/iut/main.go index 424437a..5b37447 100644 --- a/cmd/iut/main.go +++ b/cmd/iut/main.go @@ -26,9 +26,9 @@ import ( config "github.com/eiffel-community/etos-api/internal/configs/iut" "github.com/eiffel-community/etos-api/internal/iut/contextmanager" - server "github.com/eiffel-community/etos-api/internal/iut/server" "github.com/eiffel-community/etos-api/internal/logging" - "github.com/eiffel-community/etos-api/pkg/iut/application" + 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" @@ -81,7 +81,7 @@ func main() { defer v1alpha1App.Close() router := application.New(v1alpha1App) - srv := server.NewWebserver(cfg, log, router) + srv := server.NewWebService(cfg, log, router) done := make(chan os.Signal, 1) signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) diff --git a/internal/configs/iut/config.go b/internal/configs/iut/config.go index 5fb99ab..8ff88bb 100644 --- a/internal/configs/iut/config.go +++ b/internal/configs/iut/config.go @@ -31,6 +31,7 @@ type Config interface { LogLevel() string LogFilePath() string Timeout() time.Duration + ETOSNamespace() string EventRepositoryHost() string IutWaitTimeoutHard() time.Duration IutWaitTimeoutSoft() time.Duration @@ -44,6 +45,7 @@ type cfg struct { logLevel string logFilePath string timeout time.Duration + etosNamespace string databaseHost string databasePort string eventRepositoryHost string @@ -110,6 +112,11 @@ func (c *cfg) Timeout() time.Duration { return c.timeout } +// ETOSNamespace returns the ETOS namespace. +func (c *cfg) ETOSNamespace() string { + return c.etosNamespace +} + // EventRepositoryHost returns the host to use for event lookups. func (c *cfg) EventRepositoryHost() string { return c.eventRepositoryHost diff --git a/pkg/iut/application/application.go b/pkg/iut/application/application.go deleted file mode 100644 index cb4fc0d..0000000 --- a/pkg/iut/application/application.go +++ /dev/null @@ -1,35 +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 application - -import "github.com/julienschmidt/httprouter" - -type Application interface { - LoadRoutes(*httprouter.Router) - Close() -} - -// New loads routes for all applications supplied and returns a new router to -// be used in the server. -func New(application Application, applications ...Application) *httprouter.Router { - router := httprouter.New() - application.LoadRoutes(router) - for _, app := range applications { - app.LoadRoutes(router) - } - return router -} diff --git a/pkg/iut/application/application_test.go b/pkg/iut/application/application_test.go deleted file mode 100644 index 0e5d682..0000000 --- a/pkg/iut/application/application_test.go +++ /dev/null @@ -1,83 +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 application - -import ( - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/julienschmidt/httprouter" - "github.com/stretchr/testify/assert" -) - -type testApp struct { - route string - message string -} - -// testRoute is a test route that prints a test message from the app to which it is "attached". -func (t *testApp) testRoute(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - fmt.Fprint(w, t.message) -} - -// LoadRoutes from the application to which it is "attached". -func (t *testApp) LoadRoutes(router *httprouter.Router) { - router.GET(t.route, t.testRoute) -} - -// Close is a placeholder to fulfill implementation of the application interface. -func (t *testApp) Close() {} - -// TestNew verifies that it is possible to load a handlers routes. -func TestNew(t *testing.T) { - app := &testApp{ - route: "/testing", - message: "hello", - } - router := New(app) - - responseRecorder := httptest.NewRecorder() - request := httptest.NewRequest("GET", app.route, nil) - router.ServeHTTP(responseRecorder, request) - assert.Equal(t, 200, responseRecorder.Code) - assert.Equal(t, app.message, responseRecorder.Body.String()) -} - -// TestNew verifies that it is possible to load multiple handlers routes. -func TestNewMultiple(t *testing.T) { - route1 := &testApp{"/route1", "hello1"} - route2 := &testApp{"/route2", "hello2"} - tests := []struct { - name string - app *testApp - }{ - {name: "Route1", app: route1}, - {name: "Route1", app: route2}, - } - - router := New(route1, route2) - - for _, testCase := range tests { - responseRecorder := httptest.NewRecorder() - request := httptest.NewRequest("GET", testCase.app.route, nil) - router.ServeHTTP(responseRecorder, request) - assert.Equal(t, 200, responseRecorder.Code) - assert.Equal(t, testCase.app.message, responseRecorder.Body.String()) - } -} diff --git a/pkg/iut/v1alpha1/v1alpha1.go b/pkg/iut/v1alpha1/v1alpha1.go index 8dc1fc8..ee41728 100644 --- a/pkg/iut/v1alpha1/v1alpha1.go +++ b/pkg/iut/v1alpha1/v1alpha1.go @@ -26,8 +26,7 @@ import ( 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/iut/contextmanager" - "github.com/eiffel-community/etos-api/internal/iut/responses" - "github.com/eiffel-community/etos-api/pkg/iut/application" + "github.com/eiffel-community/etos-api/pkg/application" packageurl "github.com/package-url/packageurl-go" clientv3 "go.etcd.io/etcd/client/v3" @@ -36,15 +35,6 @@ import ( "github.com/sirupsen/logrus" ) -var ( - service_version string -) - -// BASEREGEX for matching /testrun/tercc-id/provider/iuts/reference. -const BASEREGEX = "/testrun/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/provider/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/iuts" - -const EtcdTreePathPrefix = "/purl" - type V1Alpha1Application struct { logger *logrus.Entry cfg config.Config @@ -62,7 +52,20 @@ type V1Alpha1Handler struct { } type Dataset struct { - Greed interface{} `json:"greed"` +} + +// 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 @@ -92,7 +95,7 @@ func (a V1Alpha1Application) LoadRoutes(router *httprouter.Router) { // Selftest is a handler to just return 204. func (h V1Alpha1Handler) Selftest(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - responses.RespondWithError(w, http.StatusNoContent, "") + RespondWithError(w, http.StatusNoContent, "") } type StartRequest struct { @@ -125,7 +128,7 @@ type StopRequest struct { Id uuid.UUID `json:"id"` } -// Start creates a StartResponse with the required number of IUTs (package URLs) +// 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() @@ -134,13 +137,12 @@ func (h V1Alpha1Handler) Start(w http.ResponseWriter, r *http.Request, ps httpro var startReq StartRequest if err := json.NewDecoder(r.Body).Decode(&startReq); err != nil { - responses.RespondWithError(w, http.StatusBadRequest, err.Error()) + RespondWithError(w, http.StatusBadRequest, err.Error()) return } - defer r.Body.Close() purl, err := packageurl.FromString(startReq.ArtifactIdentity) if err != nil { - responses.RespondWithError(w, http.StatusBadRequest, err.Error()) + RespondWithError(w, http.StatusBadRequest, err.Error()) return } @@ -150,12 +152,12 @@ func (h V1Alpha1Handler) Start(w http.ResponseWriter, r *http.Request, ps httpro } iuts, err := json.Marshal(purls) if err != nil { - responses.RespondWithError(w, http.StatusInternalServerError, err.Error()) + RespondWithError(w, http.StatusInternalServerError, err.Error()) return } _, err = h.database.Put(r.Context(), fmt.Sprintf("/iut/%s", checkOutID.String()), string(iuts)) if err != nil { - responses.RespondWithError(w, http.StatusInternalServerError, err.Error()) + RespondWithError(w, http.StatusInternalServerError, err.Error()) return } startResp := StartResponse{Id: checkOutID} @@ -164,24 +166,23 @@ func (h V1Alpha1Handler) Start(w http.ResponseWriter, r *http.Request, ps httpro _, _ = w.Write(response) } -// Status creates a simple DONE Status response to indicate IUTs have been checked out. +// 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()) - defer r.Body.Close() 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) - responses.RespondWithError(w, http.StatusInternalServerError, err.Error()) + RespondWithError(w, http.StatusInternalServerError, err.Error()) return } if len(dbResp.Kvs) == 0 { err = fmt.Errorf("No key found: %s", key) - responses.RespondWithError(w, http.StatusInternalServerError, err.Error()) + RespondWithError(w, http.StatusInternalServerError, err.Error()) return } statusResp := StatusResponse{ @@ -189,12 +190,13 @@ func (h V1Alpha1Handler) Status(w http.ResponseWriter, r *http.Request, ps httpr Status: "DONE", } if err = json.Unmarshal(dbResp.Kvs[0].Value, &statusResp.Iuts); err != nil { - responses.RespondWithError(w, http.StatusInternalServerError, err.Error()) + RespondWithError(w, http.StatusInternalServerError, err.Error()) + RespondWithError(w, http.StatusInternalServerError, err.Error()) return } response, err := json.Marshal(statusResp) if err != nil { - responses.RespondWithError(w, http.StatusInternalServerError, err.Error()) + RespondWithError(w, http.StatusInternalServerError, err.Error()) return } w.WriteHeader(http.StatusOK) @@ -207,17 +209,16 @@ func (h V1Alpha1Handler) Stop(w http.ResponseWriter, r *http.Request, ps httprou 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()) - responses.RespondWithError(w, http.StatusBadRequest, 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()) - responses.RespondWithError(w, http.StatusInternalServerError, err.Error()) + RespondWithError(w, http.StatusInternalServerError, err.Error()) return } w.WriteHeader(http.StatusNoContent) @@ -251,7 +252,7 @@ func (h V1Alpha1Handler) panicRecovery( r.Context(), ).Errorf("recovering from err %+v\n %s", err, buf) identifier := ps.ByName("identifier") - responses.RespondWithError( + RespondWithError( w, http.StatusInternalServerError, fmt.Sprintf("unknown error: contact server admin with id '%s'", identifier), From b8fb6b011b0f5fcab5203866b39b75fc4501c16c Mon Sep 17 00:00:00 2001 From: Andrei Matveyeu Date: Fri, 13 Sep 2024 10:07:19 +0200 Subject: [PATCH 3/6] restored defer r.Body.Close() in Start/Stop, contextmanager removed --- cmd/iut/main.go | 7 +- internal/iut/contextmanager/contextmanager.go | 84 ------------------- internal/iut/responses/responses.go | 35 -------- internal/iut/responses/responses_test.go | 42 ---------- pkg/iut/v1alpha1/v1alpha1.go | 12 +-- 5 files changed, 5 insertions(+), 175 deletions(-) delete mode 100644 internal/iut/contextmanager/contextmanager.go delete mode 100644 internal/iut/responses/responses.go delete mode 100644 internal/iut/responses/responses_test.go diff --git a/cmd/iut/main.go b/cmd/iut/main.go index 5b37447..e648520 100644 --- a/cmd/iut/main.go +++ b/cmd/iut/main.go @@ -25,7 +25,6 @@ import ( "time" config "github.com/eiffel-community/etos-api/internal/configs/iut" - "github.com/eiffel-community/etos-api/internal/iut/contextmanager" "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" @@ -72,12 +71,8 @@ func main() { log.WithError(err).Fatal("failed to create etcd connection") } - cm := contextmanager.New(cli) - go cm.Start(ctx) - defer cm.CancelAll() - log.Info("Loading v1alpha1 routes") - v1alpha1App := v1alpha1.New(cfg, log, ctx, cm, cli) + v1alpha1App := v1alpha1.New(cfg, log, ctx, cli) defer v1alpha1App.Close() router := application.New(v1alpha1App) diff --git a/internal/iut/contextmanager/contextmanager.go b/internal/iut/contextmanager/contextmanager.go deleted file mode 100644 index ce3e09b..0000000 --- a/internal/iut/contextmanager/contextmanager.go +++ /dev/null @@ -1,84 +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 contextmanager - -import ( - "context" - "regexp" - "strings" - - "go.etcd.io/etcd/api/v3/mvccpb" - clientv3 "go.etcd.io/etcd/client/v3" -) - -const REGEX = "/testrun/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/provider/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/status" - -// ContextManager manages contexts for the IUT provider goroutines, enabling the cancelation -// of the context in the case where a testrun should be removed during the checkout phase. -type ContextManager struct { - contexts map[string]context.CancelFunc - db *clientv3.Client -} - -// New creates a new context manager. -func New(db *clientv3.Client) *ContextManager { - return &ContextManager{ - contexts: make(map[string]context.CancelFunc), - db: db, - } -} - -// CancelAll cancels all saved contexts within the context manager. -func (c *ContextManager) CancelAll() { - for _, cancel := range c.contexts { - cancel() - } - c.contexts = make(map[string]context.CancelFunc) -} - -// Start up the context manager testrun deletaion watcher. -func (c *ContextManager) Start(ctx context.Context) { - regex := regexp.MustCompile(REGEX) - ch := c.db.Watch(ctx, "/testrun", clientv3.WithPrefix()) - for response := range ch { - for _, event := range response.Events { - if event.Type == mvccpb.DELETE { - if !regex.Match(event.Kv.Key) { - continue - } - dbPath := strings.Split(string(event.Kv.Key), "/") - c.Cancel(dbPath[2]) - } - } - } -} - -// Cancel the context for one stored ID. -func (c *ContextManager) Cancel(id string) { - cancel, ok := c.contexts[id] - if !ok { - return - } - delete(c.contexts, id) - cancel() -} - -// Add a new context to the context manager. -func (c *ContextManager) Add(ctx context.Context, id string) context.Context { - ctx, cancel := context.WithCancel(ctx) - c.contexts[id] = cancel - return ctx -} diff --git a/internal/iut/responses/responses.go b/internal/iut/responses/responses.go deleted file mode 100644 index 458ef23..0000000 --- a/internal/iut/responses/responses.go +++ /dev/null @@ -1,35 +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 responses - -import ( - "encoding/json" - "net/http" -) - -// 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}) -} diff --git a/internal/iut/responses/responses_test.go b/internal/iut/responses/responses_test.go deleted file mode 100644 index a819deb..0000000 --- a/internal/iut/responses/responses_test.go +++ /dev/null @@ -1,42 +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 responses - -import ( - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -// Test that RespondWithJSON writes the correct HTTP code, message and adds a content type header. -func TestRespondWithJSON(t *testing.T) { - responseRecorder := httptest.NewRecorder() - RespondWithJSON(responseRecorder, 200, map[string]string{"hello": "world"}) - assert.Equal(t, "application/json", responseRecorder.Header().Get("Content-Type")) - assert.Equal(t, 200, responseRecorder.Result().StatusCode) - assert.JSONEq(t, `{"hello": "world"}`, responseRecorder.Body.String()) -} - -// Test that RespondWithError writes the correct HTTP code, message and adds a content type header. -func TestRespondWithError(t *testing.T) { - responseRecorder := httptest.NewRecorder() - RespondWithError(responseRecorder, 400, "failure") - assert.Equal(t, "application/json", responseRecorder.Header().Get("Content-Type")) - assert.Equal(t, 400, responseRecorder.Result().StatusCode) - assert.JSONEq(t, `{"error": "failure"}`, responseRecorder.Body.String()) -} diff --git a/pkg/iut/v1alpha1/v1alpha1.go b/pkg/iut/v1alpha1/v1alpha1.go index ee41728..5ff8b98 100644 --- a/pkg/iut/v1alpha1/v1alpha1.go +++ b/pkg/iut/v1alpha1/v1alpha1.go @@ -25,7 +25,6 @@ import ( 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/iut/contextmanager" "github.com/eiffel-community/etos-api/pkg/application" packageurl "github.com/package-url/packageurl-go" clientv3 "go.etcd.io/etcd/client/v3" @@ -39,7 +38,6 @@ type V1Alpha1Application struct { logger *logrus.Entry cfg config.Config database *clientv3.Client - cm *contextmanager.ContextManager wg *sync.WaitGroup } @@ -47,7 +45,6 @@ type V1Alpha1Handler struct { logger *logrus.Entry cfg config.Config database *clientv3.Client - cm *contextmanager.ContextManager wg *sync.WaitGroup } @@ -74,19 +71,18 @@ func (a *V1Alpha1Application) Close() { } // New returns a new V1Alpha1Application object/struct -func New(cfg config.Config, log *logrus.Entry, ctx context.Context, cm *contextmanager.ContextManager, cli *clientv3.Client) application.Application { +func New(cfg config.Config, log *logrus.Entry, ctx context.Context, cli *clientv3.Client) application.Application { return &V1Alpha1Application{ logger: log, cfg: cfg, database: cli, - cm: cm, 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.cm, a.wg} + 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))) @@ -140,6 +136,7 @@ func (h V1Alpha1Handler) Start(w http.ResponseWriter, r *http.Request, ps httpro 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()) @@ -190,7 +187,6 @@ func (h V1Alpha1Handler) Status(w http.ResponseWriter, r *http.Request, ps httpr Status: "DONE", } if err = json.Unmarshal(dbResp.Kvs[0].Value, &statusResp.Iuts); err != nil { - RespondWithError(w, http.StatusInternalServerError, err.Error()) RespondWithError(w, http.StatusInternalServerError, err.Error()) return } @@ -209,7 +205,7 @@ func (h V1Alpha1Handler) Stop(w http.ResponseWriter, r *http.Request, ps httprou 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()) From d50ecc081096a0ffa6586326b79a1f5ed4c54eb6 Mon Sep 17 00:00:00 2001 From: Andrei Matveyeu Date: Fri, 13 Sep 2024 11:25:27 +0200 Subject: [PATCH 4/6] more unused code removed --- cmd/iut/main.go | 7 ++-- internal/configs/iut/config.go | 64 ++++------------------------- internal/configs/iut/config_test.go | 17 -------- pkg/iut/v1alpha1/v1alpha1.go | 3 +- 4 files changed, 12 insertions(+), 79 deletions(-) diff --git a/cmd/iut/main.go b/cmd/iut/main.go index e648520..19cb0bd 100644 --- a/cmd/iut/main.go +++ b/cmd/iut/main.go @@ -56,9 +56,9 @@ func main() { } log := logger.WithFields(logrus.Fields{ "hostname": hostname, - "application": "Dummy IUT Provider Service", + "application": "ETOS IUT Provider Service Mini", "version": vcsRevision(), - "name": "Dummy IUT Provider", + "name": "ETOS IUT Provider Mini", "user_log": false, }) @@ -90,14 +90,13 @@ func main() { <-done log.Info("SIGTERM received") - ctx, cancel := context.WithTimeout(ctx, cfg.Timeout()) + 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) } - log.Info("Wait for checkout, deploy, status and checkin jobs to complete") } // fileLogging adds a hook into a slice of hooks, if the filepath configuration is set diff --git a/internal/configs/iut/config.go b/internal/configs/iut/config.go index 8ff88bb..4b8fb0b 100644 --- a/internal/configs/iut/config.go +++ b/internal/configs/iut/config.go @@ -19,9 +19,6 @@ import ( "flag" "fmt" "os" - "time" - - "github.com/sirupsen/logrus" ) // Config interface for retrieving configuration options. @@ -30,58 +27,31 @@ type Config interface { ServicePort() string LogLevel() string LogFilePath() string - Timeout() time.Duration ETOSNamespace() string - EventRepositoryHost() string - IutWaitTimeoutHard() time.Duration - IutWaitTimeoutSoft() time.Duration DatabaseURI() string } // cfg implements the Config interface. type cfg struct { - serviceHost string - servicePort string - logLevel string - logFilePath string - timeout time.Duration - etosNamespace string - databaseHost string - databasePort string - eventRepositoryHost string - iutWaitTimeoutHard time.Duration - iutWaitTimeoutSoft time.Duration + 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 - defaultTimeout, err := time.ParseDuration(EnvOrDefault("REQUEST_TIMEOUT", "1m")) - if err != nil { - logrus.Panic(err) - } - - iutWaitTimeoutHard, err := time.ParseDuration(EnvOrDefault("IUT_WAIT_TIMEOUT", "1h")) - if err != nil { - logrus.Panic(err) - } - - iutWaitTimeoutSoft, err := time.ParseDuration(EnvOrDefault("IUT_WAIT_TIMEOUT_SOFT", "30m")) - 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.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 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.eventRepositoryHost, "eventrepository_url", os.Getenv("ETOS_GRAPHQL_SERVER"), "URL to the GraphQL server to use for event lookup.") - flag.DurationVar(&conf.iutWaitTimeoutHard, "hard iut wait timeout", iutWaitTimeoutHard, "Hard wait timeout for IUT checkout") - flag.DurationVar(&conf.iutWaitTimeoutSoft, "soft iut wait timeout", iutWaitTimeoutSoft, "Soft wait timeout for IUT checkout") flag.Parse() return &conf @@ -107,31 +77,11 @@ func (c *cfg) LogFilePath() string { return c.logFilePath } -// Timeout returns the request timeout for Provider Service API. -func (c *cfg) Timeout() time.Duration { - return c.timeout -} - // ETOSNamespace returns the ETOS namespace. func (c *cfg) ETOSNamespace() string { return c.etosNamespace } -// EventRepositoryHost returns the host to use for event lookups. -func (c *cfg) EventRepositoryHost() string { - return c.eventRepositoryHost -} - -// IutWaitTimeoutHard returns the hard timeout for IUT checkout -func (c *cfg) IutWaitTimeoutHard() time.Duration { - return c.iutWaitTimeoutHard -} - -// IutWaitTimeoutSoft returns the soft timeout for IUT checkout -func (c *cfg) IutWaitTimeoutSoft() time.Duration { - return c.iutWaitTimeoutSoft -} - // DatabaseURI returns the URI to the ETOS database. func (c *cfg) DatabaseURI() string { return fmt.Sprintf("%s:%s", c.databaseHost, c.databasePort) diff --git a/internal/configs/iut/config_test.go b/internal/configs/iut/config_test.go index 6ad30ca..b5039da 100644 --- a/internal/configs/iut/config_test.go +++ b/internal/configs/iut/config_test.go @@ -19,7 +19,6 @@ import ( "fmt" "os" "testing" - "time" "github.com/stretchr/testify/assert" ) @@ -30,7 +29,6 @@ func TestGet(t *testing.T) { serverHost := "127.0.0.1" logLevel := "DEBUG" logFilePath := "path/to/a/file" - timeoutStr := "1m" databaseHost := "etcd" databasePort := "12345" @@ -38,12 +36,9 @@ func TestGet(t *testing.T) { os.Setenv("SERVICE_PORT", port) os.Setenv("LOGLEVEL", logLevel) os.Setenv("LOG_FILE_PATH", logFilePath) - os.Setenv("REQUEST_TIMEOUT", timeoutStr) os.Setenv("ETOS_ETCD_HOST", databaseHost) os.Setenv("ETOS_ETCD_PORT", databasePort) - 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) @@ -52,8 +47,6 @@ func TestGet(t *testing.T) { assert.Equal(t, logFilePath, conf.logFilePath) assert.Equal(t, databaseHost, conf.databaseHost) assert.Equal(t, databasePort, conf.databasePort) - assert.Equal(t, timeout, conf.timeout) - assert.Equal(t, timeout, conf.timeout) } type getter func() string @@ -86,13 +79,3 @@ func TestGetters(t *testing.T) { }) } } - -// 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/pkg/iut/v1alpha1/v1alpha1.go b/pkg/iut/v1alpha1/v1alpha1.go index 5ff8b98..53f356c 100644 --- a/pkg/iut/v1alpha1/v1alpha1.go +++ b/pkg/iut/v1alpha1/v1alpha1.go @@ -22,6 +22,7 @@ import ( "net/http" "runtime" "sync" + "time" eiffelevents "github.com/eiffel-community/eiffelevents-sdk-go" config "github.com/eiffel-community/etos-api/internal/configs/iut" @@ -225,7 +226,7 @@ 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(), h.cfg.Timeout()) + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() newRequest := r.WithContext(ctx) fn(w, newRequest, ps) From 718156927925ba9894418ad6653d21b5e8cb28e7 Mon Sep 17 00:00:00 2001 From: Andrei Matveyeu Date: Fri, 13 Sep 2024 12:02:00 +0200 Subject: [PATCH 5/6] fix for docker compose YAML files --- deploy/etos-iut/docker-compose.yml | 4 ++-- deploy/etos-logarea/docker-compose.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/etos-iut/docker-compose.yml b/deploy/etos-iut/docker-compose.yml index 4128058..72fda81 100644 --- a/deploy/etos-iut/docker-compose.yml +++ b/deploy/etos-iut/docker-compose.yml @@ -12,5 +12,5 @@ services: ports: - 8080:8080 env_file: - - ./configs/development.env - entrypoint: ["/bin/bash", "./scripts/entrypoint.sh"] + - ../../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..51a6e4e 100644 --- a/deploy/etos-logarea/docker-compose.yml +++ b/deploy/etos-logarea/docker-compose.yml @@ -12,5 +12,5 @@ services: ports: - 8080:8080 env_file: - - ./configs/development.env - entrypoint: ["/bin/bash", "./scripts/entrypoint.sh"] + - ../../configs/development.env + entrypoint: ["/app/bin/logarea"] From fa02f8131ef087d912ae3b50aba104dbab298b26 Mon Sep 17 00:00:00 2001 From: Andrei Matveyeu Date: Fri, 13 Sep 2024 12:40:32 +0200 Subject: [PATCH 6/6] fixes for Makefile and docker compose YAMLs --- Makefile | 4 ++++ README.rst | 8 ++++++++ deploy/etos-iut/docker-compose.yml | 2 +- deploy/etos-logarea/docker-compose.yml | 2 +- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 32d1d24..341b864 100644 --- a/Makefile +++ b/Makefile @@ -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/deploy/etos-iut/docker-compose.yml b/deploy/etos-iut/docker-compose.yml index 72fda81..e525fc7 100644 --- a/deploy/etos-iut/docker-compose.yml +++ b/deploy/etos-iut/docker-compose.yml @@ -12,5 +12,5 @@ services: ports: - 8080:8080 env_file: - - ../../configs/development.env + - ./configs/development.env entrypoint: ["/app/bin/iut"] diff --git a/deploy/etos-logarea/docker-compose.yml b/deploy/etos-logarea/docker-compose.yml index 51a6e4e..64c10c6 100644 --- a/deploy/etos-logarea/docker-compose.yml +++ b/deploy/etos-logarea/docker-compose.yml @@ -12,5 +12,5 @@ services: ports: - 8080:8080 env_file: - - ../../configs/development.env + - ./configs/development.env entrypoint: ["/app/bin/logarea"]