detached a5fe447

Refactoring of app.go for dynamic OAuthCallbackURL based on config.yml settings

commit
a5fe44788349527f2e3f568270459094f8869122
author
bnema · · 2023-09-17 15:02 UTC
parent
476b35d
signature
unsigned
5 files · +152 -118
M internal/app/app.go +80 -50
@@ -2,67 +2,97 @@ package app
22
33 import (
44 "database/sql"
5+ "fmt"
56 "io/fs"
7+ "os"
68
7- "github.com/bnema/gordon/internal/gotemplate"
8- "github.com/bnema/gordon/internal/webui"
9+ "github.com/bnema/gordon/pkg/utils/docker"
10+ "github.com/bnema/gordon/pkg/utils/parser"
911 )
1012
11-const (
12- BuildVersion = "0.0.2"
13- BuildDir = "tmp"
14- // Those configs should be read from a config file
15- HttpPort = 1323
16- TopDomain = "example.com"
17- SubDomain = "gordon"
18- AdminPath = "/admin"
19- DockerSock = "/var/run/docker.sock"
20- // If enabled replace all docker commands by podman
21- PodmanEnable = false
22- PodmanSock = "/run/user/1000/podman/podman.sock"
23- OauthCallbackURL = "http://localhost:1323/admin/login/oauth/callback"
13+var (
14+ OauthCallbackURL string
15+ appEnv string
2416 )
2517
18+func init() {
19+ appEnv = os.Getenv("APP_ENV")
20+ if appEnv == "" {
21+ appEnv = "prod" // Default to "prod" if APP_ENV is not set
22+ }
23+}
24+
2625 type App struct {
27- TemplateFS fs.FS
28- PublicFS fs.FS
29- BuildVersion string
30- BuildDir string
31- DBDir string
32- DBFilename string
33- DBPath string
34- InitialChecksum string
35- DB *sql.DB
36- TopDomain string
37- SubDomain string
38- AdminPath string
39- DockerSock string
40- PodmanEnable bool
41- PodmanSock string
42- OauthCallbackURL string
43- HttpPort int16
26+ TemplateFS fs.FS
27+ PublicFS fs.FS
28+ DBDir string
29+ DBFilename string
30+ DBPath string
31+ InitialChecksum string
32+ DB *sql.DB
33+ Config AppConfig
4434 }
4535
46-func NewApp() *App {
47- return &App{
48- TemplateFS: gotemplate.TemplateFS,
49- PublicFS: webui.PublicFS,
50- BuildVersion: BuildVersion,
51- BuildDir: BuildDir,
52- DBDir: DBDir,
53- DBFilename: DBFilename,
54- TopDomain: TopDomain,
55- SubDomain: SubDomain,
56- AdminPath: AdminPath,
57- OauthCallbackURL: OauthCallbackURL,
58- HttpPort: HttpPort,
36+type AppConfig struct {
37+ RunEnv string `yaml:"runEnv"`
38+ BuildVersion string `yaml:"buildVersion"`
39+ HttpPort int `yaml:"httpPort"`
40+ TopDomain string `yaml:"topDomain"`
41+ SubDomain string `yaml:"subDomain"`
42+ AdminPath string `yaml:"adminPath"`
43+ DockerSock string `yaml:"dockerSock"`
44+ PodmanEnable bool `yaml:"podmanEnable"`
45+ PodmanSock string `yaml:"podmanSock"`
46+ OauthCallbackURL string `yaml:"oauthCallbackURL"`
47+ BuildDir string `yaml:"buildDir"`
48+}
49+
50+func InitializeEnvironment() {
51+ config, err := LoadConfig()
52+ if err != nil {
53+ fmt.Errorf("Error initializing environment: %s", err)
5954 }
55+ OauthCallbackURL = GenerateOauthCallbackURL(config)
56+}
57+
58+func LoadConfig() (AppConfig, error) {
59+ var config AppConfig
60+ buildDir, configFile := getEnvPaths()
61+ err := parser.OpenYamlFile(os.DirFS(buildDir), configFile, &config, buildDir) // Replace `nil` with your actual filesystem
62+ return config, err
63+}
64+
65+// NewDockerConfig creates and returns a new Docker client configuration based on AppConfig.
66+func (config *AppConfig) NewDockerConfig() *docker.Config {
67+ return &docker.Config{
68+ DockerSock: config.DockerSock,
69+ PodmanEnable: config.PodmanEnable,
70+ PodmanSock: config.PodmanSock,
71+ }
72+}
73+
74+func GenerateOauthCallbackURL(config AppConfig) string {
75+ var scheme, port string
76+
77+ if config.RunEnv == "dev" {
78+ scheme = "http"
79+ port = fmt.Sprintf(":%d", config.HttpPort)
80+ } else { // Assuming "prod"
81+ scheme = "https"
82+ port = "" // Assuming that HTTPS will run on the default port 443
83+ }
84+
85+ domain := config.TopDomain
86+ if config.SubDomain != "" {
87+ domain = fmt.Sprintf("%s.%s", config.SubDomain, config.TopDomain)
88+ }
89+
90+ return fmt.Sprintf("%s://%s%s%s/login/oauth/callback", scheme, domain, port, config.AdminPath)
6091 }
6192
62-func NewDockerClient() *App {
63- return &App{
64- DockerSock: DockerSock,
65- PodmanEnable: PodmanEnable,
66- PodmanSock: PodmanSock,
93+func getEnvPaths() (string, string) {
94+ if appEnv == "dev" {
95+ return "tmp/", "config.yml"
6796 }
97+ return "./", "config.yml"
6898 }
@@ -2,67 +2,97 @@ package app
2 2
3 import (3 import (
4 "database/sql"4 "database/sql"
5+ "fmt"
5 "io/fs"6 "io/fs"
7+ "os"
6 8
7- "github.com/bnema/gordon/internal/gotemplate"9+ "github.com/bnema/gordon/pkg/utils/docker"
8- "github.com/bnema/gordon/internal/webui"10+ "github.com/bnema/gordon/pkg/utils/parser"
9 )11 )
10 12
11-const (13+var (
12- BuildVersion = "0.0.2"14+ OauthCallbackURL string
13- BuildDir = "tmp"15+ appEnv string
14- // Those configs should be read from a config file
15- HttpPort = 1323
16- TopDomain = "example.com"
17- SubDomain = "gordon"
18- AdminPath = "/admin"
19- DockerSock = "/var/run/docker.sock"
20- // If enabled replace all docker commands by podman
21- PodmanEnable = false
22- PodmanSock = "/run/user/1000/podman/podman.sock"
23- OauthCallbackURL = "http://localhost:1323/admin/login/oauth/callback"
24 )16 )
25 17
18+func init() {
19+ appEnv = os.Getenv("APP_ENV")
20+ if appEnv == "" {
21+ appEnv = "prod" // Default to "prod" if APP_ENV is not set
22+ }
23+}
24+
26 type App struct {25 type App struct {
27- TemplateFS fs.FS26+ TemplateFS fs.FS
28- PublicFS fs.FS27+ PublicFS fs.FS
29- BuildVersion string28+ DBDir string
30- BuildDir string29+ DBFilename string
31- DBDir string30+ DBPath string
32- DBFilename string31+ InitialChecksum string
33- DBPath string32+ DB *sql.DB
34- InitialChecksum string33+ Config AppConfig
35- DB *sql.DB
36- TopDomain string
37- SubDomain string
38- AdminPath string
39- DockerSock string
40- PodmanEnable bool
41- PodmanSock string
42- OauthCallbackURL string
43- HttpPort int16
44 }34 }
45 35
46-func NewApp() *App {36+type AppConfig struct {
47- return &App{37+ RunEnv string `yaml:"runEnv"`
48- TemplateFS: gotemplate.TemplateFS,38+ BuildVersion string `yaml:"buildVersion"`
49- PublicFS: webui.PublicFS,39+ HttpPort int `yaml:"httpPort"`
50- BuildVersion: BuildVersion,40+ TopDomain string `yaml:"topDomain"`
51- BuildDir: BuildDir,41+ SubDomain string `yaml:"subDomain"`
52- DBDir: DBDir,42+ AdminPath string `yaml:"adminPath"`
53- DBFilename: DBFilename,43+ DockerSock string `yaml:"dockerSock"`
54- TopDomain: TopDomain,44+ PodmanEnable bool `yaml:"podmanEnable"`
55- SubDomain: SubDomain,45+ PodmanSock string `yaml:"podmanSock"`
56- AdminPath: AdminPath,46+ OauthCallbackURL string `yaml:"oauthCallbackURL"`
57- OauthCallbackURL: OauthCallbackURL,47+ BuildDir string `yaml:"buildDir"`
58- HttpPort: HttpPort,48+}
49+
50+func InitializeEnvironment() {
51+ config, err := LoadConfig()
52+ if err != nil {
53+ fmt.Errorf("Error initializing environment: %s", err)
59 }54 }
55+ OauthCallbackURL = GenerateOauthCallbackURL(config)
56+}
57+
58+func LoadConfig() (AppConfig, error) {
59+ var config AppConfig
60+ buildDir, configFile := getEnvPaths()
61+ err := parser.OpenYamlFile(os.DirFS(buildDir), configFile, &config, buildDir) // Replace `nil` with your actual filesystem
62+ return config, err
63+}
64+
65+// NewDockerConfig creates and returns a new Docker client configuration based on AppConfig.
66+func (config *AppConfig) NewDockerConfig() *docker.Config {
67+ return &docker.Config{
68+ DockerSock: config.DockerSock,
69+ PodmanEnable: config.PodmanEnable,
70+ PodmanSock: config.PodmanSock,
71+ }
72+}
73+
74+func GenerateOauthCallbackURL(config AppConfig) string {
75+ var scheme, port string
76+
77+ if config.RunEnv == "dev" {
78+ scheme = "http"
79+ port = fmt.Sprintf(":%d", config.HttpPort)
80+ } else { // Assuming "prod"
81+ scheme = "https"
82+ port = "" // Assuming that HTTPS will run on the default port 443
83+ }
84+
85+ domain := config.TopDomain
86+ if config.SubDomain != "" {
87+ domain = fmt.Sprintf("%s.%s", config.SubDomain, config.TopDomain)
88+ }
89+
90+ return fmt.Sprintf("%s://%s%s%s/login/oauth/callback", scheme, domain, port, config.AdminPath)
60 }91 }
61 92
62-func NewDockerClient() *App {93+func getEnvPaths() (string, string) {
63- return &App{94+ if appEnv == "dev" {
64- DockerSock: DockerSock,95+ return "tmp/", "config.yml"
65- PodmanEnable: PodmanEnable,
66- PodmanSock: PodmanSock,
67 }96 }
97+ return "./", "config.yml"
68 }98 }
M internal/gotemplate/render/html.go +1 -1
@@ -17,7 +17,7 @@ type Renderer struct {
1717 }
1818
1919 // Render function renders the template with the given data
20-func (r *Renderer) Render(data interface{}, a *app.App) (string, error) {
20+func (r *Renderer) Render(data interface{}, a *app.AppConfig) (string, error) {
2121 if r.ParseError != nil {
2222 return "", fmt.Errorf("failed to parse template: %w", r.ParseError)
2323
@@ -17,7 +17,7 @@ type Renderer struct {
17 }17 }
18 18
19 // Render function renders the template with the given data19 // Render function renders the template with the given data
20-func (r *Renderer) Render(data interface{}, a *app.App) (string, error) {20+func (r *Renderer) Render(data interface{}, a *app.AppConfig) (string, error) {
21 if r.ParseError != nil {21 if r.ParseError != nil {
22 return "", fmt.Errorf("failed to parse template: %w", r.ParseError)22 return "", fmt.Errorf("failed to parse template: %w", r.ParseError)
23 23
M internal/httpserve/handler/oauth.go +26 -48
@@ -1,11 +1,11 @@
11 package handler
22
33 import (
4+ "encoding/base64"
45 "fmt"
5- "io"
66 "net/http"
7- "net/url"
87 "os"
8+ "strings"
99
1010 "github.com/bnema/gordon/internal/app"
1111 "github.com/bnema/gordon/internal/gotemplate/render"
@@ -47,65 +47,43 @@ func RenderLoginPage(c echo.Context, a *app.App) error {
4747 }
4848
4949 func StartOAuthGithub(c echo.Context, a *app.App) error {
50- clientID := os.Getenv("GITHUB_CLIENTID")
51- redirectURI := a.OauthCallbackURL // Make sure this URL is registered in your GitHub OAuth App
52- fmt.Print(a.OauthCallbackURL)
53- // Define the scopes you need; for example, "repo" and "admin:repo_hook" for repository and webhook actions
54- scopes := "repo,admin:repo_hook"
55-
56- // Redirect to GitHub's OAuth page
50+ clientID := os.Getenv("GITHUB_APP_ID")
51+ redirectDomain := a.OauthCallbackURL
52+ encodedState := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain))
53+ fmt.Print("redirectDomain:", redirectDomain)
54+ fmt.Print("encoded state :", encodedState)
55+ // Redirect to Gordon's Proxy to grab the oauth access
5756 oauthURL := fmt.Sprintf(
58- "https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&scope=%s",
57+ "https://gordon.bnema.dev/github-proxy/authorize?client_id=%s&redirect_uri=%s&state=%s",
5958 clientID,
60- redirectURI,
61- scopes,
59+ redirectDomain,
60+ encodedState,
6261 )
6362 return c.Redirect(http.StatusFound, oauthURL)
6463 }
6564
6665 func OAuthCallback(c echo.Context, a *app.App) error {
67- clientID := os.Getenv("GITHUB_CLIENTID")
68- clientSecret := os.Getenv("GITHUB_TOKEN")
69- code := c.QueryParam("code")
70-
71- if code == "" {
72- return c.JSON(http.StatusBadRequest, "Missing code")
73- }
74-
75- // Create POST request to exchange code for access token
76- payload := url.Values{}
77- payload.Set("client_id", clientID)
78- payload.Set("client_secret", clientSecret)
79- payload.Set("code", code)
80-
81- resp, err := http.PostForm("https://github.com/login/oauth/access_token", payload)
82- if err != nil {
83- return c.JSON(http.StatusInternalServerError, "Failed to get access token")
84- }
85- defer resp.Body.Close()
86-
87- if resp.StatusCode != 200 {
88- return c.JSON(http.StatusInternalServerError, "Received non-200 status code")
89- }
66+ accessToken := c.QueryParam("access_token")
67+ encodedState := c.QueryParam("state")
9068
91- // Parse the response
92- body, err := io.ReadAll(resp.Body)
69+ // Decode the state parameter to get the original redirectDomain
70+ decodedState, err := base64.StdEncoding.DecodeString(encodedState)
9371 if err != nil {
94- return c.JSON(http.StatusInternalServerError, "Failed to read access token")
72+ return c.String(http.StatusBadRequest, "Invalid state parameter")
9573 }
9674
97- // Parse the access token from the response body
98- accessToken := string(body)
99-
100- parsedQuery, err := url.ParseQuery(accessToken)
101- if err != nil {
102- return c.JSON(http.StatusInternalServerError, "Failed to parse access token")
75+ state := string(decodedState)
76+ parts := strings.SplitN(state, ":", 2)
77+ if len(parts) != 2 || parts[0] != "redirectDomain" {
78+ return c.String(http.StatusBadRequest, "Invalid state format")
10379 }
10480
105- actualToken := parsedQuery.Get("access_token")
106- fmt.Println("Received access token:", actualToken)
81+ // Retrieve the original redirectDomain
82+ redirectDomain := parts[1]
83+ fmt.Print(redirectDomain)
84+ // Now, you can use accessToken for whatever you need in your application
10785
108- // Get the current session from echo-contrib/session
86+ // Also, you can save the authentication state in the session
10987 sess, err := session.Get("session", c)
11088 if err != nil {
11189 return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
@@ -113,12 +91,12 @@ func OAuthCallback(c echo.Context, a *app.App) error {
11391
11492 // Set the user as authenticated
11593 sess.Values["authenticated"] = true
94+ sess.Values["access_token"] = accessToken
11695 if err = sess.Save(c.Request(), c.Response()); err != nil {
11796 return echo.NewHTTPError(http.StatusInternalServerError, "Could not save session")
11897 }
11998
12099 return c.Redirect(http.StatusFound, "/admin")
121-
122100 }
123101
124102 // Logout handles user logout and clears session
@@ -1,11 +1,11 @@
1 package handler1 package handler
2 2
3 import (3 import (
4+ "encoding/base64"
4 "fmt"5 "fmt"
5- "io"
6 "net/http"6 "net/http"
7- "net/url"
8 "os"7 "os"
8+ "strings"
9 9
10 "github.com/bnema/gordon/internal/app"10 "github.com/bnema/gordon/internal/app"
11 "github.com/bnema/gordon/internal/gotemplate/render"11 "github.com/bnema/gordon/internal/gotemplate/render"
@@ -47,65 +47,43 @@ func RenderLoginPage(c echo.Context, a *app.App) error {
47 }47 }
48 48
49 func StartOAuthGithub(c echo.Context, a *app.App) error {49 func StartOAuthGithub(c echo.Context, a *app.App) error {
50- clientID := os.Getenv("GITHUB_CLIENTID")50+ clientID := os.Getenv("GITHUB_APP_ID")
51- redirectURI := a.OauthCallbackURL // Make sure this URL is registered in your GitHub OAuth App51+ redirectDomain := a.OauthCallbackURL
52- fmt.Print(a.OauthCallbackURL)52+ encodedState := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain))
53- // Define the scopes you need; for example, "repo" and "admin:repo_hook" for repository and webhook actions53+ fmt.Print("redirectDomain:", redirectDomain)
54- scopes := "repo,admin:repo_hook"54+ fmt.Print("encoded state :", encodedState)
55-55+ // Redirect to Gordon's Proxy to grab the oauth access
56- // Redirect to GitHub's OAuth page
57 oauthURL := fmt.Sprintf(56 oauthURL := fmt.Sprintf(
58- "https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&scope=%s",57+ "https://gordon.bnema.dev/github-proxy/authorize?client_id=%s&redirect_uri=%s&state=%s",
59 clientID,58 clientID,
60- redirectURI,59+ redirectDomain,
61- scopes,60+ encodedState,
62 )61 )
63 return c.Redirect(http.StatusFound, oauthURL)62 return c.Redirect(http.StatusFound, oauthURL)
64 }63 }
65 64
66 func OAuthCallback(c echo.Context, a *app.App) error {65 func OAuthCallback(c echo.Context, a *app.App) error {
67- clientID := os.Getenv("GITHUB_CLIENTID")66+ accessToken := c.QueryParam("access_token")
68- clientSecret := os.Getenv("GITHUB_TOKEN")67+ encodedState := c.QueryParam("state")
69- code := c.QueryParam("code")
70-
71- if code == "" {
72- return c.JSON(http.StatusBadRequest, "Missing code")
73- }
74-
75- // Create POST request to exchange code for access token
76- payload := url.Values{}
77- payload.Set("client_id", clientID)
78- payload.Set("client_secret", clientSecret)
79- payload.Set("code", code)
80-
81- resp, err := http.PostForm("https://github.com/login/oauth/access_token", payload)
82- if err != nil {
83- return c.JSON(http.StatusInternalServerError, "Failed to get access token")
84- }
85- defer resp.Body.Close()
86-
87- if resp.StatusCode != 200 {
88- return c.JSON(http.StatusInternalServerError, "Received non-200 status code")
89- }
90 68
91- // Parse the response69+ // Decode the state parameter to get the original redirectDomain
92- body, err := io.ReadAll(resp.Body)70+ decodedState, err := base64.StdEncoding.DecodeString(encodedState)
93 if err != nil {71 if err != nil {
94- return c.JSON(http.StatusInternalServerError, "Failed to read access token")72+ return c.String(http.StatusBadRequest, "Invalid state parameter")
95 }73 }
96 74
97- // Parse the access token from the response body75+ state := string(decodedState)
98- accessToken := string(body)76+ parts := strings.SplitN(state, ":", 2)
99-77+ if len(parts) != 2 || parts[0] != "redirectDomain" {
100- parsedQuery, err := url.ParseQuery(accessToken)78+ return c.String(http.StatusBadRequest, "Invalid state format")
101- if err != nil {
102- return c.JSON(http.StatusInternalServerError, "Failed to parse access token")
103 }79 }
104 80
105- actualToken := parsedQuery.Get("access_token")81+ // Retrieve the original redirectDomain
106- fmt.Println("Received access token:", actualToken)82+ redirectDomain := parts[1]
83+ fmt.Print(redirectDomain)
84+ // Now, you can use accessToken for whatever you need in your application
107 85
108- // Get the current session from echo-contrib/session86+ // Also, you can save the authentication state in the session
109 sess, err := session.Get("session", c)87 sess, err := session.Get("session", c)
110 if err != nil {88 if err != nil {
111 return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")89 return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
@@ -113,12 +91,12 @@ func OAuthCallback(c echo.Context, a *app.App) error {
113 91
114 // Set the user as authenticated92 // Set the user as authenticated
115 sess.Values["authenticated"] = true93 sess.Values["authenticated"] = true
94+ sess.Values["access_token"] = accessToken
116 if err = sess.Save(c.Request(), c.Response()); err != nil {95 if err = sess.Save(c.Request(), c.Response()); err != nil {
117 return echo.NewHTTPError(http.StatusInternalServerError, "Could not save session")96 return echo.NewHTTPError(http.StatusInternalServerError, "Could not save session")
118 }97 }
119 98
120 return c.Redirect(http.StatusFound, "/admin")99 return c.Redirect(http.StatusFound, "/admin")
121-
122 }100 }
123 101
124 // Logout handles user logout and clears session102 // Logout handles user logout and clears session
M pkg/utils/docker/initclient.go +10 -14
@@ -1,29 +1,25 @@
11 package docker
22
33 import (
4- "context"
54 "fmt"
65 "log"
76
8- "github.com/bnema/gordon/internal/app"
9- "github.com/docker/docker/api/types"
107 "github.com/docker/docker/client"
11- "github.com/docker/docker/pkg/archive"
128 )
139
14-var dockerClient *client.Client
10+type Config struct {
11+ DockerSock string
12+ PodmanEnable bool
13+ PodmanSock string
14+}
1515
16-func init() {
17- // Get Docker configuration from App
18- appInstance := app.NewDockerClient()
19- dockerSocket := appInstance.DockerSock
20- podmanEnable := appInstance.PodmanEnable
21- podmanSocket := appInstance.PodmanSock
16+var dockerClient *client.Client
2217
18+func InitializeDockerClient(config *Config) {
2319 // Based on configuration, decide whether to use Docker or Podman
24- socketPath := dockerSocket
25- if podmanEnable {
26- socketPath = podmanSocket
20+ socketPath := config.DockerSock
21+ if config.PodmanEnable {
22+ socketPath = config.PodmanSock
2723 }
2824
2925 // Initialize Docker client
@@ -1,29 +1,25 @@
1 package docker1 package docker
2 2
3 import (3 import (
4- "context"
5 "fmt"4 "fmt"
6 "log"5 "log"
7 6
8- "github.com/bnema/gordon/internal/app"
9- "github.com/docker/docker/api/types"
10 "github.com/docker/docker/client"7 "github.com/docker/docker/client"
11- "github.com/docker/docker/pkg/archive"
12 )8 )
13 9
14-var dockerClient *client.Client10+type Config struct {
11+ DockerSock string
12+ PodmanEnable bool
13+ PodmanSock string
14+}
15 15
16-func init() {16+var dockerClient *client.Client
17- // Get Docker configuration from App
18- appInstance := app.NewDockerClient()
19- dockerSocket := appInstance.DockerSock
20- podmanEnable := appInstance.PodmanEnable
21- podmanSocket := appInstance.PodmanSock
22 17
18+func InitializeDockerClient(config *Config) {
23 // Based on configuration, decide whether to use Docker or Podman19 // Based on configuration, decide whether to use Docker or Podman
24- socketPath := dockerSocket20+ socketPath := config.DockerSock
25- if podmanEnable {21+ if config.PodmanEnable {
26- socketPath = podmanSocket22+ socketPath = config.PodmanSock
27 }23 }
28 24
29 // Initialize Docker client25 // Initialize Docker client
M pkg/utils/parser/yaml.go +35 -5
@@ -4,28 +4,58 @@ import (
44 "fmt"
55 "io"
66 "io/fs"
7+ "path/filepath"
78
89 "gopkg.in/yaml.v3"
910 )
1011
12+// // OpenYamlFile opens a YAML file and unmarshals it into the out interface
13+// func OpenYamlFile(fs fs.FS, filename string, out interface{}) error {
14+// // Open the YAML file
15+// file, err := fs.Open(filename)
16+// if err != nil {
17+// return fmt.Errorf("failed to open %s: %w", filename, err)
18+// }
19+// defer file.Close()
20+
21+// // Read the content of the file
22+// content, err := io.ReadAll(file)
23+// if err != nil {
24+// return fmt.Errorf("failed to read %s: %w", filename, err)
25+// }
26+
27+// // Unmarshal the YAML content into the out interface
28+// if err := yaml.Unmarshal(content, out); err != nil {
29+// return fmt.Errorf("failed to unmarshal %s: %w", filename, err)
30+// }
31+
32+// return nil
33+// }
34+
1135 // OpenYamlFile opens a YAML file and unmarshals it into the out interface
12-func OpenYamlFile(fs fs.FS, filename string, out interface{}) error {
36+func OpenYamlFile(fs fs.FS, filename string, out interface{}, dir ...string) error {
37+ // Construct the full path of the YAML file
38+ fullPath := filename
39+ if len(dir) > 0 {
40+ fullPath = filepath.Join(dir[0], filename)
41+ }
42+
1343 // Open the YAML file
14- file, err := fs.Open(filename)
44+ file, err := fs.Open(fullPath)
1545 if err != nil {
16- return fmt.Errorf("failed to open %s: %w", filename, err)
46+ return fmt.Errorf("failed to open %s: %w", fullPath, err)
1747 }
1848 defer file.Close()
1949
2050 // Read the content of the file
2151 content, err := io.ReadAll(file)
2252 if err != nil {
23- return fmt.Errorf("failed to read %s: %w", filename, err)
53+ return fmt.Errorf("failed to read %s: %w", fullPath, err)
2454 }
2555
2656 // Unmarshal the YAML content into the out interface
2757 if err := yaml.Unmarshal(content, out); err != nil {
28- return fmt.Errorf("failed to unmarshal %s: %w", filename, err)
58+ return fmt.Errorf("failed to unmarshal YAML: %w", err)
2959 }
3060
3161 return nil
@@ -4,28 +4,58 @@ import (
4 "fmt"4 "fmt"
5 "io"5 "io"
6 "io/fs"6 "io/fs"
7+ "path/filepath"
7 8
8 "gopkg.in/yaml.v3"9 "gopkg.in/yaml.v3"
9 )10 )
10 11
12+// // OpenYamlFile opens a YAML file and unmarshals it into the out interface
13+// func OpenYamlFile(fs fs.FS, filename string, out interface{}) error {
14+// // Open the YAML file
15+// file, err := fs.Open(filename)
16+// if err != nil {
17+// return fmt.Errorf("failed to open %s: %w", filename, err)
18+// }
19+// defer file.Close()
20+
21+// // Read the content of the file
22+// content, err := io.ReadAll(file)
23+// if err != nil {
24+// return fmt.Errorf("failed to read %s: %w", filename, err)
25+// }
26+
27+// // Unmarshal the YAML content into the out interface
28+// if err := yaml.Unmarshal(content, out); err != nil {
29+// return fmt.Errorf("failed to unmarshal %s: %w", filename, err)
30+// }
31+
32+// return nil
33+// }
34+
11 // OpenYamlFile opens a YAML file and unmarshals it into the out interface35 // OpenYamlFile opens a YAML file and unmarshals it into the out interface
12-func OpenYamlFile(fs fs.FS, filename string, out interface{}) error {36+func OpenYamlFile(fs fs.FS, filename string, out interface{}, dir ...string) error {
37+ // Construct the full path of the YAML file
38+ fullPath := filename
39+ if len(dir) > 0 {
40+ fullPath = filepath.Join(dir[0], filename)
41+ }
42+
13 // Open the YAML file43 // Open the YAML file
14- file, err := fs.Open(filename)44+ file, err := fs.Open(fullPath)
15 if err != nil {45 if err != nil {
16- return fmt.Errorf("failed to open %s: %w", filename, err)46+ return fmt.Errorf("failed to open %s: %w", fullPath, err)
17 }47 }
18 defer file.Close()48 defer file.Close()
19 49
20 // Read the content of the file50 // Read the content of the file
21 content, err := io.ReadAll(file)51 content, err := io.ReadAll(file)
22 if err != nil {52 if err != nil {
23- return fmt.Errorf("failed to read %s: %w", filename, err)53+ return fmt.Errorf("failed to read %s: %w", fullPath, err)
24 }54 }
25 55
26 // Unmarshal the YAML content into the out interface56 // Unmarshal the YAML content into the out interface
27 if err := yaml.Unmarshal(content, out); err != nil {57 if err := yaml.Unmarshal(content, out); err != nil {
28- return fmt.Errorf("failed to unmarshal %s: %w", filename, err)58+ return fmt.Errorf("failed to unmarshal YAML: %w", err)
29 }59 }
30 60
31 return nil61 return nil

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close