From bfdb1c2bf74caf31148de25b6c6c382bc2f21bb6 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Mon, 6 Jun 2022 12:55:39 +0200 Subject: [PATCH] Introduce remote-challenge-sync-airbus --- .drone.yml | 30 ++--- Dockerfile-remote-challenge-sync-airbus | 24 ++++ fickit-frontend.yml | 11 +- remote/challenge-sync-airbus/.gitignore | 1 + remote/challenge-sync-airbus/api.go | 166 ++++++++++++++++++++++++ remote/challenge-sync-airbus/main.go | 98 ++++++++++++++ 6 files changed, 309 insertions(+), 21 deletions(-) create mode 100644 Dockerfile-remote-challenge-sync-airbus create mode 100644 remote/challenge-sync-airbus/.gitignore create mode 100644 remote/challenge-sync-airbus/api.go create mode 100644 remote/challenge-sync-airbus/main.go diff --git a/.drone.yml b/.drone.yml index 66191483..d1c64991 100644 --- a/.drone.yml +++ b/.drone.yml @@ -256,6 +256,21 @@ steps: branch: - master + - name: docker remote-challenge-sync-airbus + image: plugins/docker + settings: + username: + from_secret: docker_username + password: + from_secret: docker_password + repo: nemunaire/fic-remote-challenge-sync-airbus + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + dockerfile: Dockerfile-remote-challenge-sync-airbus + when: + branch: + - master + trigger: event: - cron @@ -557,21 +572,6 @@ steps: password: from_secret: docker_password - - name: docker remote-scores-sync-zqds - image: plugins/docker - settings: - username: - from_secret: docker_username - password: - from_secret: docker_password - repo: nemunaire/fic-remote-scores-sync-zqds - auto_tag: true - auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} - dockerfile: Dockerfile-remote-scores-sync-zqds - when: - branch: - - master - trigger: event: - push diff --git a/Dockerfile-remote-challenge-sync-airbus b/Dockerfile-remote-challenge-sync-airbus new file mode 100644 index 00000000..aef39743 --- /dev/null +++ b/Dockerfile-remote-challenge-sync-airbus @@ -0,0 +1,24 @@ +FROM golang:1-alpine as gobuild + +RUN apk add --no-cache git + +WORKDIR /go/src/srs.epita.fr/fic-server/ + +COPY go.mod go.sum ./ +COPY libfic ./libfic/ +COPY settings ./settings/ +COPY remote/challenge-sync-airbus ./remote/challenge-sync-airbus/ + +RUN go get -d -v ./remote/challenge-sync-airbus && \ + go build -v -buildvcs=false -o ./challenge-sync-airbus ./remote/challenge-sync-airbus + + +FROM alpine:3.16 + +RUN apk add --no-cache openssl ca-certificates + +WORKDIR /srv + +ENTRYPOINT ["/srv/challenge-sync-airbus"] + +COPY --from=gobuild /go/src/srs.epita.fr/fic-server/challenge-sync-airbus /srv/challenge-sync-airbus diff --git a/fickit-frontend.yml b/fickit-frontend.yml index f30d3359..efc394b1 100644 --- a/fickit-frontend.yml +++ b/fickit-frontend.yml @@ -192,13 +192,12 @@ services: - /var/lib/fic/startingblock - /var/lib/fic/submissions - /var/lib/fic/teams - - name: fic-remote-scores-sync-zqds - image: nemunaire/fic-remote-scores-sync-zqds:latest + - name: fic-remote-challenge-sync-airbus + image: nemunaire/fic-remote-challenge-sync-airbus:latest env: - - ZQDS_EVENTID=6109ae5acbb7b36b789c9330 - - ZQDS_ROUNDID=612d3a5179fe4f747ea89274 - - ZQDS_CLIENTID= - - ZQDS_CLIENTSECRET= + - AIRBUS_BASEURL=https://.... + - AIRBUS_TOKEN=abcdef0123456789abcdef0123456789 + - AIRBUS_SESSIONID=42 binds: - /etc/hosts:/etc/hosts:ro - /var/lib/fic/teams:/srv/TEAMS:ro diff --git a/remote/challenge-sync-airbus/.gitignore b/remote/challenge-sync-airbus/.gitignore new file mode 100644 index 00000000..d46d7700 --- /dev/null +++ b/remote/challenge-sync-airbus/.gitignore @@ -0,0 +1 @@ +challenge-sync-airbus \ No newline at end of file diff --git a/remote/challenge-sync-airbus/api.go b/remote/challenge-sync-airbus/api.go new file mode 100644 index 00000000..9b01cfe5 --- /dev/null +++ b/remote/challenge-sync-airbus/api.go @@ -0,0 +1,166 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" +) + +type AirbusAPI struct { + BaseURL string + Token string + SessionID uint64 +} + +func (a *AirbusAPI) request(method, endpoint string, data []byte, out interface{}) error { + var reader *bytes.Reader + if data != nil { + reader = bytes.NewReader(data) + } + req, err := http.NewRequest(method, a.BaseURL+endpoint, reader) + if err != nil { + return fmt.Errorf("unable to prepare request to %q: %w", endpoint, err) + } + + req.Header.Add("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("error during request execution to %q: %w", endpoint, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + if out != nil { + jdec := json.NewDecoder(resp.Body) + + if err := jdec.Decode(out); err != nil { + return fmt.Errorf("an error occurs when trying to decode response: %w", err) + } + } + } else if all, err := io.ReadAll(resp.Body); err != nil { + return fmt.Errorf("error returned by the API + error on decoding: %d // %w", resp.StatusCode, err) + } else { + return fmt.Errorf("error returned by the API: %d -> %s", resp.StatusCode, all) + } + + return nil +} + +type AirbusUserId int64 + +func (aui AirbusUserId) String() string { + return strconv.FormatInt(int64(aui), 10) +} + +type AirbusUser struct { + Id AirbusUserId `json:"id"` + Name string `json:"name"` +} + +func (a *AirbusAPI) GetUsers() (users []AirbusUser, err error) { + err = a.request("GET", fmt.Sprintf("/sessions/%d/users", a.SessionID), nil, &users) + return +} + +func (a *AirbusAPI) GetUserFromName(name string) (*AirbusUser, error) { + users, err := a.GetUsers() + if err != nil { + return nil, fmt.Errorf("unable to retrieve users list: %w", err) + } + + for _, u := range users { + if u.Name == name { + return &u, nil + } + } + + return nil, fmt.Errorf("unable to find user %q", name) +} + +type AirbusChallengeId int64 + +func (aci AirbusChallengeId) String() string { + return strconv.FormatInt(int64(aci), 10) +} + +type AirbusChallenge struct { + Id AirbusChallengeId `json:"id"` + Name string `json:"name"` +} + +func (a *AirbusAPI) GetChallenges() (challenges []AirbusChallenge, err error) { + err = a.request("GET", fmt.Sprintf("/sessions/%d/challenges", a.SessionID), nil, &challenges) + return +} + +func (a *AirbusAPI) GetChallengeFromName(name string) (*AirbusChallenge, error) { + challenges, err := a.GetChallenges() + if err != nil { + return nil, fmt.Errorf("unable to retrieve challenges list: %w", err) + } + + for _, c := range challenges { + if c.Name == name { + return &c, nil + } + } + + return nil, fmt.Errorf("unable to find challenge %q", name) +} + +func (a *AirbusAPI) ValidateChallengeFromUser(user *AirbusUser, challenge *AirbusChallenge) (err error) { + err = a.request("GET", fmt.Sprintf("/sessions/%d/%s/%s/validate", a.SessionID, challenge.Id.String(), user.Id.String()), nil, nil) + return +} + +type AirbusUserAwards struct { + UserId AirbusUserId `json:"gaming_user_id"` + Message string `json:"name"` + Value int64 `json:"value"` +} + +func (a *AirbusAPI) AwardUser(user *AirbusUser, value int64, message string) (err error) { + awards := AirbusUserAwards{ + UserId: user.Id, + Message: message, + Value: value, + } + + j, err := json.Marshal(awards) + if err != nil { + return fmt.Errorf("unable to marshall JSON from awards struct: %w", err) + } + + err = a.request("POST", fmt.Sprintf("/sessions/%d/awards", a.SessionID), j, nil) + return +} + +type AirbusStats struct { + Data AirbusStatsData `json:"data"` +} + +type AirbusStatsData struct { + BySessions []AirbusStatsSession `json:"by_sessions"` +} + +type AirbusStatsSession struct { + UUID AirbusUUID `json:"uuid"` + Name string `json:"name"` + Duration string `json:"duration"` + TeamStats []AirbusTeamStats `json:"team_stats"` +} + +type AirbusTeamStats struct { + UUID AirbusUUID `json:"uuid"` + Name string `json:"name"` + Score int64 `json:"score"` +} + +func (a *AirbusAPI) GetCurrentStats() (stats AirbusStats, err error) { + err = a.request("GET", "/stats", nil, &stats) + return +} diff --git a/remote/challenge-sync-airbus/main.go b/remote/challenge-sync-airbus/main.go new file mode 100644 index 00000000..3c873c10 --- /dev/null +++ b/remote/challenge-sync-airbus/main.go @@ -0,0 +1,98 @@ +package main + +import ( + "flag" + "log" + "os" + "os/signal" + "path" + "strconv" + "syscall" + + "gopkg.in/fsnotify.v1" +) + +var ( + TeamsDir string + skipInitialSync bool +) + +func main() { + flag.StringVar(&TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files") + var debugINotify = flag.Bool("debuginotify", false, "Show skipped inotofy events") + flag.BoolVar(&skipInitialSync, "skipinitialsync", skipInitialSync, "Skip the initial synchronization") + flag.Parse() + + api := AirbusAPI{ + BaseURL: "https://portal.european-cybercup.lan/api/v1", + } + + if v, exists := os.LookupEnv("AIRBUS_BASEURL"); exists { + api.BaseURL = v + } + if v, exists := os.LookupEnv("AIRBUS_TOKEN"); exists { + api.Token = v + } + if v, exists := os.LookupEnv("AIRBUS_SESSIONID"); exists { + var err error + api.SessionID, err = strconv.ParseUint(v, 10, 64) + if err != nil { + log.Fatal("AIRBUS_SESSIONID is invalid: ", err.Error()) + } + } + + log.SetPrefix("[challenge-sync-airbus] ") + + TeamsDir = path.Clean(TeamsDir) + + log.Println("Registering directory events...") + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + defer watcher.Close() + + if err := watcher.Add(TeamsDir); err != nil { + log.Fatal(err) + } + + if !skipInitialSync { + if _, err := os.Stat(path.Join(TeamsDir, "teams.json")); err == nil { + treatAll(path.Join(TeamsDir, "teams.json")) + } + } + + // Register SIGUSR1, SIGUSR2 + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, syscall.SIGHUP) + + watchedNotify := fsnotify.Create + + for { + select { + case <-interrupt: + log.Println("SIGHUP received, resyncing all teams' score...") + treatAll(path.Join(TeamsDir, "teams.json")) + log.Println("SIGHUP treated.") + case ev := <-watcher.Events: + if path.Base(ev.Name) == "teams.json" { + if ev.Op&watchedNotify == watchedNotify { + if *debugINotify { + log.Println("Treating event:", ev, "for", ev.Name) + } + go treatDiff(ev.Name) + } else if ev.Op&fsnotify.Write == fsnotify.Write { + log.Println("FSNOTIFY WRITE SEEN. Prefer looking at them, as it appears files are not atomically moved.") + watchedNotify = fsnotify.Write + go treatDiff(ev.Name) + } else if *debugINotify { + log.Println("Skipped teams.json event:", ev) + } + } else if *debugINotify { + log.Println("Skipped NON teams.json event:", ev, "for", ev.Name) + } + case err := <-watcher.Errors: + log.Println("error:", err) + } + } +}