Skip to content

Commit 46980d0

Browse files
committed
feat: docker image with env variable binding
1 parent cfe589c commit 46980d0

File tree

7 files changed

+181
-87
lines changed

7 files changed

+181
-87
lines changed

.github/workflows/release.yaml

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
name: goreleaser
22
on:
33
push:
4-
tags:
5-
- '*'
4+
tags: ['v*']
65
permissions:
76
contents: write
87
packages: write
@@ -13,11 +12,23 @@ jobs:
1312
- uses: actions/checkout@v3
1413
with:
1514
fetch-depth: 0
15+
1616
- run: git fetch --force --tags
17+
1718
- uses: actions/setup-go@v3
1819
with:
1920
go-version: '>=1.20.1'
2021
cache: true
22+
23+
- name: Set up Docker Buildx
24+
uses: docker/setup-buildx-action@v3
25+
- name: Log in to the Container registry
26+
uses: docker/login-action@v3
27+
with:
28+
registry: ${{ env.REGISTRY }}
29+
username: ${{ github.actor }}
30+
password: ${{ secrets.GITHUB_TOKEN }}
31+
2132
- uses: goreleaser/goreleaser-action@v4
2233
with:
2334
distribution: goreleaser

.goreleaser.yaml

+12
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ nfpms:
5555
bindir: /usr/local/bin
5656
universal_binaries:
5757
- replace: false
58+
dockers:
59+
- image_templates:
60+
- "ghcr.io/heig-lherman/gaps-cli:{{ .Tag }}"
61+
- "ghcr.io/heig-lherman/gaps-cli:v{{ .Major }}"
62+
- "ghcr.io/heig-lherman/gaps-cli:v{{ .Major }}.{{ .Minor }}"
63+
- "ghcr.io/heig-lherman/gaps-cli:latest"
64+
build_flag_templates:
65+
- "--pull"
66+
- "--label=org.opencontainers.image.created={{.Date}}"
67+
- "--label=org.opencontainers.image.title={{.ProjectName}}"
68+
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
69+
- "--label=org.opencontainers.image.version={{.Version}}"
5870
release:
5971
github:
6072
owner: heig-lherman

Dockerfile

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM scratch
2+
3+
ENV GAPS_LOGIN_USERNAME="" \
4+
GAPS_LOGIN_PASSWORD="" \
5+
GAPS_HISTORY_GRADES_FILE="/history/grades.json" \
6+
GAPS_SCRAPER_API_URL="" \
7+
GAPS_SCRAPER_API_KEY=""
8+
9+
ENTRYPOINT ["/gaps-cli"]
10+
COPY gaps-cli /
11+
12+
CMD ["--help"]

cmd/login.go

+28-30
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,6 @@ import (
1212
"time"
1313
)
1414

15-
const (
16-
UsernameViperKey = "login.username"
17-
PasswordViperKey = "login.password"
18-
TokenValueKey = "login.token.value"
19-
TokenStudentIdKey = "login.token.studentId"
20-
TokenDateValueKey = "login.token.generatedAt"
21-
)
22-
2315
type LoginCmdOpts struct {
2416
changePassword bool
2517
}
@@ -30,7 +22,7 @@ var (
3022
Use: "login",
3123
Short: "Allows to login to GAPS for future commands",
3224
Run: func(cmd *cobra.Command, args []string) {
33-
if credentialsViper.GetString(TokenValueKey) != "" && !loginOpts.changePassword {
25+
if credentialsViper.GetString(TokenValueViperKey.Key()) != "" && !loginOpts.changePassword {
3426
// Default session duration is 6 hours on GAPS
3527
if !isTokenExpired() {
3628
log.Info("User already logged in, keeping existing token")
@@ -43,24 +35,24 @@ var (
4335
var username string
4436
var password string
4537

46-
if defaultViper.GetString(UsernameViperKey) == "" {
38+
if defaultViper.GetString(UsernameViperKey.Key()) == "" {
4739
fmt.Print("Enter your HEIG-VD einet AAI username: ")
4840
reader := bufio.NewReader(os.Stdin)
4941
un, err := reader.ReadString('\n')
5042
username = un[:len(un)-1]
5143
util.CheckErr(err)
5244
} else {
53-
username = defaultViper.GetString(UsernameViperKey)
45+
username = defaultViper.GetString(UsernameViperKey.Key())
5446
}
5547

56-
if credentialsViper.GetString(PasswordViperKey) == "" || loginOpts.changePassword {
48+
if credentialsViper.GetString(PasswordViperKey.Key()) == "" || loginOpts.changePassword {
5749
fmt.Print("Enter your HEIG-VD einet AAI password: ")
5850
passwordBytes, err := term.ReadPassword(int(os.Stdin.Fd()))
5951
password = string(passwordBytes)
6052
fmt.Println("ok")
6153
util.CheckErr(err)
6254
} else {
63-
password = credentialsViper.GetString(PasswordViperKey)
55+
password = credentialsViper.GetString(PasswordViperKey.Key())
6456
}
6557

6658
refreshToken(username, password)
@@ -69,29 +61,31 @@ var (
6961
)
7062

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

76-
defaultViper.BindPFlag(UsernameViperKey, loginCmd.Flags().Lookup("username"))
77-
credentialsViper.BindPFlag(PasswordViperKey, loginCmd.Flags().Lookup("password"))
78-
credentialsViper.SetDefault(TokenValueKey, "")
79-
defaultViper.SetDefault(TokenStudentIdKey, -1)
80-
defaultViper.SetDefault(TokenDateValueKey, time.Now().UnixMilli())
66+
loginCmd.Flags().StringP(UsernameViperKey.Flag(), "u", "", "einet aai username (if not provided, you will be prompted to enter it)")
67+
defaultViper.BindPFlag(UsernameViperKey.Key(), loginCmd.Flags().Lookup(UsernameViperKey.Flag()))
68+
69+
loginCmd.Flags().String(PasswordViperKey.Flag(), "", "einet aai password (if not provided, you will be prompted to enter it)")
70+
credentialsViper.BindPFlag(PasswordViperKey.Key(), loginCmd.Flags().Lookup(PasswordViperKey.Flag()))
71+
72+
credentialsViper.SetDefault(TokenValueViperKey.Key(), "")
73+
defaultViper.SetDefault(TokenStudentIdViperKey.Key(), -1)
74+
defaultViper.SetDefault(TokenDateValueViperKey.Key(), time.Now().UnixMilli())
8175

8276
rootCmd.AddCommand(loginCmd)
8377
}
8478

8579
func isTokenExpired() bool {
86-
return time.Now().UnixMilli()-defaultViper.GetInt64(TokenDateValueKey) > 6*60*60*1000
80+
return time.Now().UnixMilli()-defaultViper.GetInt64(TokenDateValueViperKey.Key()) > 6*60*60*1000
8781
}
8882

8983
func refreshToken(username string, password string) {
90-
defaultViper.Set(UsernameViperKey, username)
91-
credentialsViper.Set(PasswordViperKey, password)
84+
defaultViper.Set(UsernameViperKey.Key(), username)
85+
credentialsViper.Set(PasswordViperKey.Key(), password)
9286

9387
cfg := new(gaps.ClientConfiguration)
94-
cfg.Init(defaultViper.GetString(UrlViperKey))
88+
cfg.Init(defaultViper.GetString(UrlViperKey.Key()))
9589

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

109-
credentialsViper.Set(TokenValueKey, token)
110-
defaultViper.Set(TokenStudentIdKey, studentId)
111-
defaultViper.Set(TokenDateValueKey, time.Now().UnixMilli())
103+
credentialsViper.Set(TokenValueViperKey.Key(), token)
104+
defaultViper.Set(TokenStudentIdViperKey.Key(), studentId)
105+
defaultViper.Set(TokenDateValueViperKey.Key(), time.Now().UnixMilli())
112106

113107
log.Debug("saving config")
114108
writeConfig()
115109
}
116110

117111
func buildTokenClientConfiguration() *gaps.TokenClientConfiguration {
118-
if credentialsViper.GetString(TokenValueKey) == "" {
112+
if credentialsViper.GetString(TokenValueViperKey.Key()) == "" {
119113
log.Fatal("No token found, please login first")
120114
}
121115

122116
// if token is expired, refresh it
123117
if isTokenExpired() {
124118
log.Info("Token expired, attempting refresh")
125-
refreshToken(defaultViper.GetString(UsernameViperKey), credentialsViper.GetString(PasswordViperKey))
119+
refreshToken(defaultViper.GetString(UsernameViperKey.Key()), credentialsViper.GetString(PasswordViperKey.Key()))
126120
}
127121

128122
cfg := new(gaps.TokenClientConfiguration)
129-
cfg.InitToken(defaultViper.GetString(UrlViperKey), credentialsViper.GetString(TokenValueKey), defaultViper.GetUint(TokenStudentIdKey))
123+
cfg.InitToken(
124+
defaultViper.GetString(UrlViperKey.Key()),
125+
credentialsViper.GetString(TokenValueViperKey.Key()),
126+
defaultViper.GetUint(TokenStudentIdViperKey.Key()),
127+
)
130128

131129
return cfg
132130
}

cmd/root.go

+87-38
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,72 @@
11
package cmd
22

33
import (
4+
"fmt"
45
log "github.com/sirupsen/logrus"
56
"github.com/spf13/cobra"
7+
"github.com/spf13/pflag"
68
"github.com/spf13/viper"
79
"lutonite.dev/gaps-cli/util"
810
"os"
11+
"strings"
912
)
1013

14+
type ViperKey string
15+
16+
func viperKey(key string, flag string) ViperKey {
17+
k := ViperKey(key)
18+
flagMapping[flag] = k
19+
return k
20+
}
21+
22+
func (k ViperKey) Key() string {
23+
return string(k)
24+
}
25+
func (k ViperKey) Flag() string {
26+
for flag, v := range flagMapping {
27+
if v == k {
28+
return flag
29+
}
30+
}
31+
32+
return ""
33+
}
34+
1135
const (
12-
UrlViperKey = "url"
36+
envPrefix = "GAPS"
37+
)
38+
39+
var (
40+
UrlViperKey = viperKey("url", "url")
41+
GradesHistoryFileViperKey = viperKey("history.grades.file", "history")
42+
UsernameViperKey = viperKey("login.username", "username")
43+
PasswordViperKey = viperKey("login.password", "password")
44+
ScraperApiUrlViperKey = viperKey("scraper.api.url", "api-url")
45+
ScraperApiKeyViperKey = viperKey("scraper.api.key", "api-key")
46+
TokenValueViperKey = viperKey("login.token.value", "")
47+
TokenStudentIdViperKey = viperKey("login.token.studentId", "")
48+
TokenDateValueViperKey = viperKey("login.token.generatedAt", "")
49+
50+
flagMapping = make(map[string]ViperKey)
1351
)
1452

1553
var (
1654
defaultViper = viper.New()
1755
credentialsViper = viper.New()
1856

1957
cfgFile string
58+
credsFile string
2059
loggerLevel string
2160

2261
rootCmd = &cobra.Command{
2362
Use: "gaps-cli",
2463
Short: "CLI for GAPS (Gaps is an Academical Planification System)",
64+
PersistentPreRun: func(cmd *cobra.Command, args []string) {
65+
initializeConfig(cmd)
66+
},
67+
PersistentPostRun: func(cmd *cobra.Command, args []string) {
68+
writeConfig()
69+
},
2570
}
2671
)
2772

@@ -32,14 +77,26 @@ func Execute() {
3277
}
3378

3479
func init() {
35-
cobra.OnInitialize(initConfig)
36-
3780
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "auth config file (default is $HOME/.config/gaps-cli/gaps.yaml)")
81+
rootCmd.PersistentFlags().StringVar(&credsFile, "credentials", "", "credentials config file (default is $HOME/.config/gaps-cli/credentials.yaml)")
3882
rootCmd.PersistentFlags().StringVar(&loggerLevel, "log-level", "error", "logging level")
39-
rootCmd.PersistentFlags().String(UrlViperKey, "", "GAPS URL (default is https://gaps.heig-vd.ch/)")
83+
rootCmd.PersistentFlags().String(UrlViperKey.Flag(), "", "GAPS URL (default is https://gaps.heig-vd.ch/)")
4084

41-
defaultViper.BindPFlag(UrlViperKey, rootCmd.PersistentFlags().Lookup(UrlViperKey))
42-
defaultViper.SetDefault(UrlViperKey, "https://gaps.heig-vd.ch")
85+
defaultViper.BindPFlag(UrlViperKey.Key(), rootCmd.PersistentFlags().Lookup(UrlViperKey.Flag()))
86+
defaultViper.SetDefault(UrlViperKey.Key(), "https://gaps.heig-vd.ch")
87+
}
88+
89+
func initializeConfig(cmd *cobra.Command) {
90+
if loggerLevel != "" {
91+
level, err := log.ParseLevel(loggerLevel)
92+
util.CheckErr(err)
93+
log.SetLevel(level)
94+
log.Tracef("log level set to %s", level)
95+
}
96+
97+
configDir := getConfigDirectory()
98+
initViper(cmd, defaultViper, "gaps", configDir, cfgFile)
99+
initViper(cmd, credentialsViper, "credentials", configDir, credsFile)
43100
}
44101

45102
func getConfigDirectory() string {
@@ -54,13 +111,15 @@ func getConfigDirectory() string {
54111
return configDir
55112
}
56113

57-
func initViper(v *viper.Viper, name string) {
58-
v.AddConfigPath(getConfigDirectory())
59-
v.SetConfigType("yaml")
60-
v.SetConfigName("gaps-cli/" + name)
61-
}
114+
func initViper(cmd *cobra.Command, v *viper.Viper, name string, configDir string, path string) {
115+
if path != "" {
116+
v.SetConfigFile(cfgFile)
117+
} else {
118+
v.AddConfigPath(configDir)
119+
v.SetConfigType("yaml")
120+
v.SetConfigName("gaps-cli/" + name)
121+
}
62122

63-
func bootstrapConfigFile(v *viper.Viper) {
64123
log.Debugf("writing config file %s", v.ConfigFileUsed())
65124
if err := v.SafeWriteConfig(); err != nil {
66125
util.CheckErrExcept(err, viper.ConfigFileAlreadyExistsError(""))
@@ -69,35 +128,25 @@ func bootstrapConfigFile(v *viper.Viper) {
69128
if err := v.ReadInConfig(); err == nil {
70129
log.WithField("file", v.ConfigFileUsed()).Infof("Reading global config file")
71130
}
131+
132+
v.SetEnvPrefix(envPrefix)
133+
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
134+
v.AutomaticEnv()
135+
136+
bindFlags(cmd, v)
137+
}
138+
139+
func bindFlags(cmd *cobra.Command, v *viper.Viper) {
140+
cmd.Flags().VisitAll(func(f *pflag.Flag) {
141+
configName := flagMapping[f.Name]
142+
if !f.Changed && v.IsSet(configName.Key()) {
143+
val := v.Get(configName.Key())
144+
cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val))
145+
}
146+
})
72147
}
73148

74149
func writeConfig() {
75150
defaultViper.WriteConfig()
76151
credentialsViper.WriteConfig()
77152
}
78-
79-
func initConfig() {
80-
if loggerLevel != "" {
81-
level, err := log.ParseLevel(loggerLevel)
82-
util.CheckErr(err)
83-
log.SetLevel(level)
84-
log.Tracef("log level set to %s", level)
85-
}
86-
87-
if cfgFile != "" {
88-
defaultViper.SetConfigFile(cfgFile)
89-
} else {
90-
initViper(defaultViper, "gaps")
91-
}
92-
93-
initViper(credentialsViper, "credentials")
94-
95-
defaultViper.SetEnvPrefix("gaps")
96-
defaultViper.AutomaticEnv()
97-
98-
credentialsViper.SetEnvPrefix("gaps")
99-
credentialsViper.AutomaticEnv()
100-
101-
bootstrapConfigFile(defaultViper)
102-
bootstrapConfigFile(credentialsViper)
103-
}

0 commit comments

Comments
 (0)