Skip to content

Commit d846bf5

Browse files
Refactor garm command, add password reset, cleanup
This change does a bunch of stuff: * Migrates the garm command to cobra command. This means that the -config flag will require two dashes instead of 1. So this will potentially break systemd units, but it can't be helped. * Add an admin subcommand that allows listing users and resetting the password for the admin user. * Removes old code that migrated github credentials from the config to the DB. It's time to let that bit go into the sunset. * Completely random convert spaces to tabs for the runner install templates because I forgot to do that in the previous PR. Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
1 parent 5de7636 commit d846bf5

11 files changed

Lines changed: 715 additions & 1098 deletions

File tree

cmd/garm/cmd/admin.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright 2026 Cloudbase Solutions SRL
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
// not use this file except in compliance with the License. You may obtain
5+
// a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
// License for the specific language governing permissions and limitations
13+
// under the License.
14+
15+
package cmd
16+
17+
import (
18+
"context"
19+
"errors"
20+
"fmt"
21+
22+
"github.com/jedib0t/go-pretty/v6/table"
23+
"github.com/nbutton23/zxcvbn-go"
24+
"github.com/spf13/cobra"
25+
26+
runnerErrors "github.com/cloudbase/garm-provider-common/errors"
27+
"github.com/cloudbase/garm-provider-common/util"
28+
"github.com/cloudbase/garm/config"
29+
"github.com/cloudbase/garm/database"
30+
dbCommon "github.com/cloudbase/garm/database/common"
31+
"github.com/cloudbase/garm/database/watcher"
32+
"github.com/cloudbase/garm/params"
33+
)
34+
35+
var newPassword string
36+
37+
var adminCmd = &cobra.Command{
38+
Use: "admin",
39+
Short: "Administrative commands for managing the GARM server",
40+
}
41+
42+
var adminListCmd = &cobra.Command{
43+
Use: "list",
44+
Short: "List admin users",
45+
SilenceUsage: true,
46+
RunE: func(cmd *cobra.Command, _ []string) error {
47+
ctx := cmd.Context()
48+
db, err := openDatabase(ctx)
49+
if err != nil {
50+
return err
51+
}
52+
53+
user, err := db.GetAdminUser(ctx)
54+
if err != nil {
55+
if errors.Is(err, runnerErrors.ErrNotFound) {
56+
fmt.Println("No admin users found.")
57+
return nil
58+
}
59+
return fmt.Errorf("fetching admin user: %w", err)
60+
}
61+
62+
t := table.NewWriter()
63+
t.AppendHeader(table.Row{"ID", "Username", "Email", "Enabled"})
64+
t.AppendRow(table.Row{user.ID, user.Username, user.Email, user.Enabled})
65+
fmt.Println(t.Render())
66+
return nil
67+
},
68+
}
69+
70+
var adminPasswordResetCmd = &cobra.Command{
71+
Use: "password-reset [flags] <username>",
72+
Short: "Reset the password for an admin user",
73+
Args: cobra.ExactArgs(1),
74+
SilenceUsage: true,
75+
RunE: func(cmd *cobra.Command, args []string) error {
76+
username := args[0]
77+
ctx := cmd.Context()
78+
79+
db, err := openDatabase(ctx)
80+
if err != nil {
81+
return err
82+
}
83+
84+
user, err := db.GetUser(ctx, username)
85+
if err != nil {
86+
return fmt.Errorf("fetching user %q: %w", username, err)
87+
}
88+
if !user.IsAdmin {
89+
return fmt.Errorf("user %q is not an admin", username)
90+
}
91+
92+
strength := zxcvbn.PasswordStrength(newPassword, nil)
93+
if strength.Score < 4 {
94+
return fmt.Errorf("password is too weak (score: %d/4, minimum required: 4)", strength.Score)
95+
}
96+
97+
hashed, err := util.PaswsordToBcrypt(newPassword)
98+
if err != nil {
99+
return fmt.Errorf("hashing password: %w", err)
100+
}
101+
102+
if _, err := db.UpdateUser(ctx, username, params.UpdateUserParams{
103+
Password: hashed,
104+
}); err != nil {
105+
return fmt.Errorf("updating password: %w", err)
106+
}
107+
108+
fmt.Printf("Password for user %q has been reset successfully.\n", username)
109+
return nil
110+
},
111+
}
112+
113+
func init() {
114+
adminPasswordResetCmd.Flags().StringVar(&newPassword, "new-password", "", "new password for the admin user")
115+
if err := adminPasswordResetCmd.MarkFlagRequired("new-password"); err != nil {
116+
panic(err)
117+
}
118+
119+
adminCmd.AddCommand(adminListCmd)
120+
adminCmd.AddCommand(adminPasswordResetCmd)
121+
rootCmd.AddCommand(adminCmd)
122+
}
123+
124+
// openDatabase loads the GARM config and opens a direct database connection.
125+
func openDatabase(ctx context.Context) (dbCommon.Store, error) {
126+
cfg, err := config.NewConfig(cfgFile)
127+
if err != nil {
128+
return nil, fmt.Errorf("loading config: %w", err)
129+
}
130+
131+
watcher.InitWatcher(ctx)
132+
133+
db, err := database.NewDatabase(ctx, cfg.Database)
134+
if err != nil {
135+
return nil, fmt.Errorf("opening database: %w", err)
136+
}
137+
138+
return db, nil
139+
}

cmd/garm/cmd/root.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2026 Cloudbase Solutions SRL
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
// not use this file except in compliance with the License. You may obtain
5+
// a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
// License for the specific language governing permissions and limitations
13+
// under the License.
14+
15+
package cmd
16+
17+
import (
18+
"os"
19+
20+
"github.com/spf13/cobra"
21+
22+
"github.com/cloudbase/garm/util/appdefaults"
23+
)
24+
25+
var cfgFile string
26+
27+
var rootCmd = &cobra.Command{
28+
Use: "garm",
29+
Short: "GitHub Actions Runner Manager",
30+
Long: "GitHub Actions Runner Manager (GARM) - A self hosted runners manager for GitHub and Gitea Actions.",
31+
SilenceUsage: true,
32+
Version: appdefaults.GetVersion(),
33+
RunE: func(_ *cobra.Command, _ []string) error {
34+
return runServer()
35+
},
36+
}
37+
38+
func Execute() {
39+
if err := rootCmd.Execute(); err != nil {
40+
os.Exit(1)
41+
}
42+
}
43+
44+
func init() {
45+
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", appdefaults.DefaultConfigFilePath, "path to garm config file")
46+
rootCmd.SetVersionTemplate("{{.Version}}\n")
47+
}

0 commit comments

Comments
 (0)