Skip to content

Commit

Permalink
feat: docker image with env variable binding
Browse files Browse the repository at this point in the history
  • Loading branch information
Lutonite committed Feb 25, 2024
1 parent cfe589c commit 46980d0
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 87 deletions.
15 changes: 13 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
name: goreleaser
on:
push:
tags:
- '*'
tags: ['v*']
permissions:
contents: write
packages: write
Expand All @@ -13,11 +12,23 @@ jobs:
- uses: actions/checkout@v3
with:
fetch-depth: 0

- run: git fetch --force --tags

- uses: actions/setup-go@v3
with:
go-version: '>=1.20.1'
cache: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: goreleaser/goreleaser-action@v4
with:
distribution: goreleaser
Expand Down
12 changes: 12 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ nfpms:
bindir: /usr/local/bin
universal_binaries:
- replace: false
dockers:
- image_templates:
- "ghcr.io/heig-lherman/gaps-cli:{{ .Tag }}"
- "ghcr.io/heig-lherman/gaps-cli:v{{ .Major }}"
- "ghcr.io/heig-lherman/gaps-cli:v{{ .Major }}.{{ .Minor }}"
- "ghcr.io/heig-lherman/gaps-cli:latest"
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
release:
github:
owner: heig-lherman
Expand Down
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM scratch

ENV GAPS_LOGIN_USERNAME="" \
GAPS_LOGIN_PASSWORD="" \
GAPS_HISTORY_GRADES_FILE="/history/grades.json" \
GAPS_SCRAPER_API_URL="" \
GAPS_SCRAPER_API_KEY=""

ENTRYPOINT ["/gaps-cli"]
COPY gaps-cli /

CMD ["--help"]
58 changes: 28 additions & 30 deletions cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ import (
"time"
)

const (
UsernameViperKey = "login.username"
PasswordViperKey = "login.password"
TokenValueKey = "login.token.value"
TokenStudentIdKey = "login.token.studentId"
TokenDateValueKey = "login.token.generatedAt"
)

type LoginCmdOpts struct {
changePassword bool
}
Expand All @@ -30,7 +22,7 @@ var (
Use: "login",
Short: "Allows to login to GAPS for future commands",
Run: func(cmd *cobra.Command, args []string) {
if credentialsViper.GetString(TokenValueKey) != "" && !loginOpts.changePassword {
if credentialsViper.GetString(TokenValueViperKey.Key()) != "" && !loginOpts.changePassword {
// Default session duration is 6 hours on GAPS
if !isTokenExpired() {
log.Info("User already logged in, keeping existing token")
Expand All @@ -43,24 +35,24 @@ var (
var username string
var password string

if defaultViper.GetString(UsernameViperKey) == "" {
if defaultViper.GetString(UsernameViperKey.Key()) == "" {
fmt.Print("Enter your HEIG-VD einet AAI username: ")
reader := bufio.NewReader(os.Stdin)
un, err := reader.ReadString('\n')
username = un[:len(un)-1]
util.CheckErr(err)
} else {
username = defaultViper.GetString(UsernameViperKey)
username = defaultViper.GetString(UsernameViperKey.Key())
}

if credentialsViper.GetString(PasswordViperKey) == "" || loginOpts.changePassword {
if credentialsViper.GetString(PasswordViperKey.Key()) == "" || loginOpts.changePassword {
fmt.Print("Enter your HEIG-VD einet AAI password: ")
passwordBytes, err := term.ReadPassword(int(os.Stdin.Fd()))
password = string(passwordBytes)
fmt.Println("ok")
util.CheckErr(err)
} else {
password = credentialsViper.GetString(PasswordViperKey)
password = credentialsViper.GetString(PasswordViperKey.Key())
}

refreshToken(username, password)
Expand All @@ -69,29 +61,31 @@ var (
)

func init() {
loginCmd.Flags().StringP("username", "u", "", "einet aai username (if not provided, you will be prompted to enter it)")
loginCmd.Flags().String("password", "", "einet aai password (if not provided, you will be prompted to enter it)")
loginCmd.Flags().BoolVar(&loginOpts.changePassword, "clear-password", false, "reset the password stored in the config file (if any)")

defaultViper.BindPFlag(UsernameViperKey, loginCmd.Flags().Lookup("username"))
credentialsViper.BindPFlag(PasswordViperKey, loginCmd.Flags().Lookup("password"))
credentialsViper.SetDefault(TokenValueKey, "")
defaultViper.SetDefault(TokenStudentIdKey, -1)
defaultViper.SetDefault(TokenDateValueKey, time.Now().UnixMilli())
loginCmd.Flags().StringP(UsernameViperKey.Flag(), "u", "", "einet aai username (if not provided, you will be prompted to enter it)")
defaultViper.BindPFlag(UsernameViperKey.Key(), loginCmd.Flags().Lookup(UsernameViperKey.Flag()))

loginCmd.Flags().String(PasswordViperKey.Flag(), "", "einet aai password (if not provided, you will be prompted to enter it)")
credentialsViper.BindPFlag(PasswordViperKey.Key(), loginCmd.Flags().Lookup(PasswordViperKey.Flag()))

credentialsViper.SetDefault(TokenValueViperKey.Key(), "")
defaultViper.SetDefault(TokenStudentIdViperKey.Key(), -1)
defaultViper.SetDefault(TokenDateValueViperKey.Key(), time.Now().UnixMilli())

rootCmd.AddCommand(loginCmd)
}

func isTokenExpired() bool {
return time.Now().UnixMilli()-defaultViper.GetInt64(TokenDateValueKey) > 6*60*60*1000
return time.Now().UnixMilli()-defaultViper.GetInt64(TokenDateValueViperKey.Key()) > 6*60*60*1000
}

func refreshToken(username string, password string) {
defaultViper.Set(UsernameViperKey, username)
credentialsViper.Set(PasswordViperKey, password)
defaultViper.Set(UsernameViperKey.Key(), username)
credentialsViper.Set(PasswordViperKey.Key(), password)

cfg := new(gaps.ClientConfiguration)
cfg.Init(defaultViper.GetString(UrlViperKey))
cfg.Init(defaultViper.GetString(UrlViperKey.Key()))

log.Debug("fetching token...")
login := gaps.NewLoginAction(cfg, username, password)
Expand All @@ -106,27 +100,31 @@ func refreshToken(username string, password string) {
log.Tracef("Token: %s", token)
log.Tracef("Student Id: %d", studentId)

credentialsViper.Set(TokenValueKey, token)
defaultViper.Set(TokenStudentIdKey, studentId)
defaultViper.Set(TokenDateValueKey, time.Now().UnixMilli())
credentialsViper.Set(TokenValueViperKey.Key(), token)
defaultViper.Set(TokenStudentIdViperKey.Key(), studentId)
defaultViper.Set(TokenDateValueViperKey.Key(), time.Now().UnixMilli())

log.Debug("saving config")
writeConfig()
}

func buildTokenClientConfiguration() *gaps.TokenClientConfiguration {
if credentialsViper.GetString(TokenValueKey) == "" {
if credentialsViper.GetString(TokenValueViperKey.Key()) == "" {
log.Fatal("No token found, please login first")
}

// if token is expired, refresh it
if isTokenExpired() {
log.Info("Token expired, attempting refresh")
refreshToken(defaultViper.GetString(UsernameViperKey), credentialsViper.GetString(PasswordViperKey))
refreshToken(defaultViper.GetString(UsernameViperKey.Key()), credentialsViper.GetString(PasswordViperKey.Key()))
}

cfg := new(gaps.TokenClientConfiguration)
cfg.InitToken(defaultViper.GetString(UrlViperKey), credentialsViper.GetString(TokenValueKey), defaultViper.GetUint(TokenStudentIdKey))
cfg.InitToken(
defaultViper.GetString(UrlViperKey.Key()),
credentialsViper.GetString(TokenValueViperKey.Key()),
defaultViper.GetUint(TokenStudentIdViperKey.Key()),
)

return cfg
}
125 changes: 87 additions & 38 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,72 @@
package cmd

import (
"fmt"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"lutonite.dev/gaps-cli/util"
"os"
"strings"
)

type ViperKey string

func viperKey(key string, flag string) ViperKey {
k := ViperKey(key)
flagMapping[flag] = k
return k
}

func (k ViperKey) Key() string {
return string(k)
}
func (k ViperKey) Flag() string {
for flag, v := range flagMapping {
if v == k {
return flag
}
}

return ""
}

const (
UrlViperKey = "url"
envPrefix = "GAPS"
)

var (
UrlViperKey = viperKey("url", "url")
GradesHistoryFileViperKey = viperKey("history.grades.file", "history")
UsernameViperKey = viperKey("login.username", "username")
PasswordViperKey = viperKey("login.password", "password")
ScraperApiUrlViperKey = viperKey("scraper.api.url", "api-url")
ScraperApiKeyViperKey = viperKey("scraper.api.key", "api-key")
TokenValueViperKey = viperKey("login.token.value", "")
TokenStudentIdViperKey = viperKey("login.token.studentId", "")
TokenDateValueViperKey = viperKey("login.token.generatedAt", "")

flagMapping = make(map[string]ViperKey)
)

var (
defaultViper = viper.New()
credentialsViper = viper.New()

cfgFile string
credsFile string
loggerLevel string

rootCmd = &cobra.Command{
Use: "gaps-cli",
Short: "CLI for GAPS (Gaps is an Academical Planification System)",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
initializeConfig(cmd)
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
writeConfig()
},
}
)

Expand All @@ -32,14 +77,26 @@ func Execute() {
}

func init() {
cobra.OnInitialize(initConfig)

rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "auth config file (default is $HOME/.config/gaps-cli/gaps.yaml)")
rootCmd.PersistentFlags().StringVar(&credsFile, "credentials", "", "credentials config file (default is $HOME/.config/gaps-cli/credentials.yaml)")
rootCmd.PersistentFlags().StringVar(&loggerLevel, "log-level", "error", "logging level")
rootCmd.PersistentFlags().String(UrlViperKey, "", "GAPS URL (default is https://gaps.heig-vd.ch/)")
rootCmd.PersistentFlags().String(UrlViperKey.Flag(), "", "GAPS URL (default is https://gaps.heig-vd.ch/)")

defaultViper.BindPFlag(UrlViperKey, rootCmd.PersistentFlags().Lookup(UrlViperKey))
defaultViper.SetDefault(UrlViperKey, "https://gaps.heig-vd.ch")
defaultViper.BindPFlag(UrlViperKey.Key(), rootCmd.PersistentFlags().Lookup(UrlViperKey.Flag()))
defaultViper.SetDefault(UrlViperKey.Key(), "https://gaps.heig-vd.ch")
}

func initializeConfig(cmd *cobra.Command) {
if loggerLevel != "" {
level, err := log.ParseLevel(loggerLevel)
util.CheckErr(err)
log.SetLevel(level)
log.Tracef("log level set to %s", level)
}

configDir := getConfigDirectory()
initViper(cmd, defaultViper, "gaps", configDir, cfgFile)
initViper(cmd, credentialsViper, "credentials", configDir, credsFile)
}

func getConfigDirectory() string {
Expand All @@ -54,13 +111,15 @@ func getConfigDirectory() string {
return configDir
}

func initViper(v *viper.Viper, name string) {
v.AddConfigPath(getConfigDirectory())
v.SetConfigType("yaml")
v.SetConfigName("gaps-cli/" + name)
}
func initViper(cmd *cobra.Command, v *viper.Viper, name string, configDir string, path string) {
if path != "" {
v.SetConfigFile(cfgFile)
} else {
v.AddConfigPath(configDir)
v.SetConfigType("yaml")
v.SetConfigName("gaps-cli/" + name)
}

func bootstrapConfigFile(v *viper.Viper) {
log.Debugf("writing config file %s", v.ConfigFileUsed())
if err := v.SafeWriteConfig(); err != nil {
util.CheckErrExcept(err, viper.ConfigFileAlreadyExistsError(""))
Expand All @@ -69,35 +128,25 @@ func bootstrapConfigFile(v *viper.Viper) {
if err := v.ReadInConfig(); err == nil {
log.WithField("file", v.ConfigFileUsed()).Infof("Reading global config file")
}

v.SetEnvPrefix(envPrefix)
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
v.AutomaticEnv()

bindFlags(cmd, v)
}

func bindFlags(cmd *cobra.Command, v *viper.Viper) {
cmd.Flags().VisitAll(func(f *pflag.Flag) {
configName := flagMapping[f.Name]
if !f.Changed && v.IsSet(configName.Key()) {
val := v.Get(configName.Key())
cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val))
}
})
}

func writeConfig() {
defaultViper.WriteConfig()
credentialsViper.WriteConfig()
}

func initConfig() {
if loggerLevel != "" {
level, err := log.ParseLevel(loggerLevel)
util.CheckErr(err)
log.SetLevel(level)
log.Tracef("log level set to %s", level)
}

if cfgFile != "" {
defaultViper.SetConfigFile(cfgFile)
} else {
initViper(defaultViper, "gaps")
}

initViper(credentialsViper, "credentials")

defaultViper.SetEnvPrefix("gaps")
defaultViper.AutomaticEnv()

credentialsViper.SetEnvPrefix("gaps")
credentialsViper.AutomaticEnv()

bootstrapConfigFile(defaultViper)
bootstrapConfigFile(credentialsViper)
}
Loading

0 comments on commit 46980d0

Please sign in to comment.