Skip to content

Commit fd8c13b

Browse files
committed
[CLI] implement forgejo-cli
(cherry picked from commit 2555e315f7561302484b15576d34c5da0d4cdb12) (cherry picked from commit 51b9c9092e21a451695ee0154e7d49753574f525) [CLI] implement forgejo-cli (squash) support initDB (cherry picked from commit 5c31ae602a45f1d9a90b86bece5393bc9faddf25) (cherry picked from commit bbf76489a73bad83d68ca7c8e7a75cf8e27b2198) Conflicts: because of d0dbe52 upgrade to https://pkg.go.dev/github.com/urfave/cli/v2 (cherry picked from commit b6c1bcc008fcff0e297d570a0069bf41bc74e53d) [CLI] implement forgejo-cli actions (cherry picked from commit 08be2b226e46d9f41e08f66e936b317bcfb4a257) (cherry picked from commit b6cfa88c6e2ae00e30c832ce4cf93c9e3f2cd6e4) (cherry picked from commit 59704200de59b65a4f37c39569a3b43e1ee38862) [CLI] implement forgejo-cli actions generate-secret (cherry picked from commit 6f7905c8ecf17d5f74ac9a71a453d6768c212b6d) (cherry picked from commit e085d6d2737e6238a4ff00f19f40cf839ac16b34) [CLI] implement forgejo-cli actions generate-secret (squash) NoInit (cherry picked from commit 962c944eb20268a394030495c3caab3e3d4bd8b7) [CLI] implement forgejo-cli actions register (cherry picked from commit 2f95143000e4ccc94ef14332777b58fe778edbd6) (cherry picked from commit 42f2f8731e876564b6627a43a248f262f50c04cd) [CLI] implement forgejo-cli actions register (squash) no private Do not go through the private API, directly modify the database (cherry picked from commit 1ba7c0d39d0ecd190b7d9c517bd26af6c84341aa) [CLI] implement forgejo-cli actions (cherry picked from commit 6f7905c8ecf17d5f74ac9a71a453d6768c212b6d) (cherry picked from commit e085d6d2737e6238a4ff00f19f40cf839ac16b34) [CLI] implement forgejo-cli actions generate-secret (squash) NoInit (cherry picked from commit 962c944eb20268a394030495c3caab3e3d4bd8b7) (cherry picked from commit 4c121ef022597e66d902c17e0f46839c26924b18) Conflicts: cmd/forgejo/actions.go tests/integration/cmd_forgejo_actions_test.go (cherry picked from commit 36997a48e38286579850abe4b55e75a235b56537) [CLI] implement forgejo-cli actions (squash) restore --version Refs: https://codeberg.org/forgejo/forgejo/issues/1134 (cherry picked from commit 9739eb52d8f94d32f61068d7209958e8d2582818) [CI] implement forgejo-cli (squash) the actions subcommand needs config (cherry picked from commit def638475122a26082ab3835842c84cd03839154) Conflicts: cmd/main.go https://codeberg.org/forgejo/forgejo/pulls/1209 (cherry picked from commit a1758a391043123903607338cb11490161ac946d) (cherry picked from commit 935fa650c77b151752a58f621d846b166b97cd79) (cherry picked from commit cd21026bc94922043dce8e2a5baba68111d1e569) (cherry picked from commit 1700b8973a58f0fc3469492d8a39b931019d2461) (cherry picked from commit 1def42a37945cfe88947803f9afe9468fb8798fe) (cherry picked from commit 839d97521d59a012b06e6c2b9b0655c56b41b6cd)
1 parent 00327b9 commit fd8c13b

File tree

10 files changed

+825
-2
lines changed

10 files changed

+825
-2
lines changed

cmd/forgejo/actions.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
// Copyright The Forgejo Authors.
2+
// SPDX-License-Identifier: MIT
3+
4+
package forgejo
5+
6+
import (
7+
"context"
8+
"encoding/hex"
9+
"fmt"
10+
"io"
11+
"os"
12+
"strings"
13+
14+
actions_model "code.gitea.io/gitea/models/actions"
15+
"code.gitea.io/gitea/modules/private"
16+
"code.gitea.io/gitea/modules/setting"
17+
private_routers "code.gitea.io/gitea/routers/private"
18+
19+
"github.com/urfave/cli/v2"
20+
)
21+
22+
func CmdActions(ctx context.Context) *cli.Command {
23+
return &cli.Command{
24+
Name: "actions",
25+
Usage: "Commands for managing Forgejo Actions",
26+
Subcommands: []*cli.Command{
27+
SubcmdActionsGenerateRunnerToken(ctx),
28+
SubcmdActionsGenerateRunnerSecret(ctx),
29+
SubcmdActionsRegister(ctx),
30+
},
31+
}
32+
}
33+
34+
func SubcmdActionsGenerateRunnerToken(ctx context.Context) *cli.Command {
35+
return &cli.Command{
36+
Name: "generate-runner-token",
37+
Usage: "Generate a new token for a runner to use to register with the server",
38+
Action: prepareWorkPathAndCustomConf(ctx, func(cliCtx *cli.Context) error { return RunGenerateActionsRunnerToken(ctx, cliCtx) }),
39+
Flags: []cli.Flag{
40+
&cli.StringFlag{
41+
Name: "scope",
42+
Aliases: []string{"s"},
43+
Value: "",
44+
Usage: "{owner}[/{repo}] - leave empty for a global runner",
45+
},
46+
},
47+
}
48+
}
49+
50+
func SubcmdActionsGenerateRunnerSecret(ctx context.Context) *cli.Command {
51+
return &cli.Command{
52+
Name: "generate-secret",
53+
Usage: "Generate a secret suitable for input to the register subcommand",
54+
Action: func(cliCtx *cli.Context) error { return RunGenerateSecret(ctx, cliCtx) },
55+
}
56+
}
57+
58+
func SubcmdActionsRegister(ctx context.Context) *cli.Command {
59+
return &cli.Command{
60+
Name: "register",
61+
Usage: "Idempotent registration of a runner using a shared secret",
62+
Action: prepareWorkPathAndCustomConf(ctx, func(cliCtx *cli.Context) error { return RunRegister(ctx, cliCtx) }),
63+
Flags: []cli.Flag{
64+
&cli.StringFlag{
65+
Name: "secret",
66+
Usage: "the secret the runner will use to connect as a 40 character hexadecimal string",
67+
},
68+
&cli.StringFlag{
69+
Name: "secret-stdin",
70+
Usage: "the secret the runner will use to connect as a 40 character hexadecimal string, read from stdin",
71+
},
72+
&cli.StringFlag{
73+
Name: "secret-file",
74+
Usage: "path to the file containing the secret the runner will use to connect as a 40 character hexadecimal string",
75+
},
76+
&cli.StringFlag{
77+
Name: "scope",
78+
Aliases: []string{"s"},
79+
Value: "",
80+
Usage: "{owner}[/{repo}] - leave empty for a global runner",
81+
},
82+
&cli.StringFlag{
83+
Name: "labels",
84+
Value: "",
85+
Usage: "comma separated list of labels supported by the runner (e.g. docker,ubuntu-latest,self-hosted) (not required since v1.21)",
86+
},
87+
&cli.StringFlag{
88+
Name: "name",
89+
Value: "runner",
90+
Usage: "name of the runner (default runner)",
91+
},
92+
&cli.StringFlag{
93+
Name: "version",
94+
Value: "",
95+
Usage: "version of the runner (not required since v1.21)",
96+
},
97+
},
98+
}
99+
}
100+
101+
func readSecret(ctx context.Context, cliCtx *cli.Context) (string, error) {
102+
if cliCtx.IsSet("secret") {
103+
return cliCtx.String("secret"), nil
104+
}
105+
if cliCtx.IsSet("secret-stdin") {
106+
buf, err := io.ReadAll(ContextGetStdin(ctx))
107+
if err != nil {
108+
return "", err
109+
}
110+
return string(buf), nil
111+
}
112+
if cliCtx.IsSet("secret-file") {
113+
path := cliCtx.String("secret-file")
114+
buf, err := os.ReadFile(path)
115+
if err != nil {
116+
return "", err
117+
}
118+
return string(buf), nil
119+
}
120+
return "", fmt.Errorf("at least one of the --secret, --secret-stdin, --secret-file options is required")
121+
}
122+
123+
func validateSecret(secret string) error {
124+
secretLen := len(secret)
125+
if secretLen != 40 {
126+
return fmt.Errorf("the secret must be exactly 40 characters long, not %d: generate-secret can provide a secret matching the requirements", secretLen)
127+
}
128+
if _, err := hex.DecodeString(secret); err != nil {
129+
return fmt.Errorf("the secret must be an hexadecimal string: %w", err)
130+
}
131+
return nil
132+
}
133+
134+
func RunRegister(ctx context.Context, cliCtx *cli.Context) error {
135+
if !ContextGetNoInit(ctx) {
136+
var cancel context.CancelFunc
137+
ctx, cancel = installSignals(ctx)
138+
defer cancel()
139+
140+
if err := initDB(ctx); err != nil {
141+
return err
142+
}
143+
}
144+
setting.MustInstalled()
145+
146+
secret, err := readSecret(ctx, cliCtx)
147+
if err != nil {
148+
return err
149+
}
150+
if err := validateSecret(secret); err != nil {
151+
return err
152+
}
153+
scope := cliCtx.String("scope")
154+
labels := cliCtx.String("labels")
155+
name := cliCtx.String("name")
156+
version := cliCtx.String("version")
157+
158+
//
159+
// There are two kinds of tokens
160+
//
161+
// - "registration token" only used when a runner interacts to
162+
// register
163+
//
164+
// - "token" obtained after a successful registration and stored by
165+
// the runner to authenticate
166+
//
167+
// The register subcommand does not need a "registration token", it
168+
// needs a "token". Using the same name is confusing and secret is
169+
// preferred for this reason in the cli.
170+
//
171+
// The ActionsRunnerRegister argument is token to be consistent with
172+
// the internal naming. It is still confusing to the developer but
173+
// not to the user.
174+
//
175+
owner, repo, err := private_routers.ParseScope(ctx, scope)
176+
if err != nil {
177+
return err
178+
}
179+
180+
runner, err := actions_model.RegisterRunner(ctx, owner, repo, secret, strings.Split(labels, ","), name, version)
181+
if err != nil {
182+
return fmt.Errorf("error while registering runner: %v", err)
183+
}
184+
185+
if _, err := fmt.Fprintf(ContextGetStdout(ctx), "%s", runner.UUID); err != nil {
186+
panic(err)
187+
}
188+
return nil
189+
}
190+
191+
func RunGenerateSecret(ctx context.Context, cliCtx *cli.Context) error {
192+
runner := actions_model.ActionRunner{}
193+
if err := runner.GenerateToken(); err != nil {
194+
return err
195+
}
196+
if _, err := fmt.Fprintf(ContextGetStdout(ctx), "%s", runner.Token); err != nil {
197+
panic(err)
198+
}
199+
return nil
200+
}
201+
202+
func RunGenerateActionsRunnerToken(ctx context.Context, cliCtx *cli.Context) error {
203+
if !ContextGetNoInit(ctx) {
204+
var cancel context.CancelFunc
205+
ctx, cancel = installSignals(ctx)
206+
defer cancel()
207+
}
208+
209+
setting.MustInstalled()
210+
211+
scope := cliCtx.String("scope")
212+
213+
respText, extra := private.GenerateActionsRunnerToken(ctx, scope)
214+
if extra.HasError() {
215+
return handleCliResponseExtra(ctx, extra)
216+
}
217+
if _, err := fmt.Fprintf(ContextGetStdout(ctx), "%s", respText); err != nil {
218+
panic(err)
219+
}
220+
return nil
221+
}
222+
223+
func prepareWorkPathAndCustomConf(ctx context.Context, action cli.ActionFunc) func(cliCtx *cli.Context) error {
224+
return func(cliCtx *cli.Context) error {
225+
if !ContextGetNoInit(ctx) {
226+
var args setting.ArgWorkPathAndCustomConf
227+
// from children to parent, check the global flags
228+
for _, curCtx := range cliCtx.Lineage() {
229+
if curCtx.IsSet("work-path") && args.WorkPath == "" {
230+
args.WorkPath = curCtx.String("work-path")
231+
}
232+
if curCtx.IsSet("custom-path") && args.CustomPath == "" {
233+
args.CustomPath = curCtx.String("custom-path")
234+
}
235+
if curCtx.IsSet("config") && args.CustomConf == "" {
236+
args.CustomConf = curCtx.String("config")
237+
}
238+
}
239+
setting.InitWorkPathAndCommonConfig(os.Getenv, args)
240+
}
241+
return action(cliCtx)
242+
}
243+
}

cmd/forgejo/forgejo.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright The Forgejo Authors.
2+
// SPDX-License-Identifier: MIT
3+
4+
package forgejo
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"io"
10+
"os"
11+
"os/signal"
12+
"syscall"
13+
14+
"code.gitea.io/gitea/models/db"
15+
"code.gitea.io/gitea/modules/log"
16+
"code.gitea.io/gitea/modules/private"
17+
"code.gitea.io/gitea/modules/setting"
18+
19+
"github.com/urfave/cli/v2"
20+
)
21+
22+
type key int
23+
24+
const (
25+
noInitKey key = iota + 1
26+
noExitKey
27+
stdoutKey
28+
stderrKey
29+
stdinKey
30+
)
31+
32+
func CmdForgejo(ctx context.Context) *cli.Command {
33+
return &cli.Command{
34+
Name: "forgejo-cli",
35+
Usage: "Forgejo CLI",
36+
Flags: []cli.Flag{},
37+
Subcommands: []*cli.Command{
38+
CmdActions(ctx),
39+
},
40+
}
41+
}
42+
43+
func ContextSetNoInit(ctx context.Context, value bool) context.Context {
44+
return context.WithValue(ctx, noInitKey, value)
45+
}
46+
47+
func ContextGetNoInit(ctx context.Context) bool {
48+
value, ok := ctx.Value(noInitKey).(bool)
49+
return ok && value
50+
}
51+
52+
func ContextSetNoExit(ctx context.Context, value bool) context.Context {
53+
return context.WithValue(ctx, noExitKey, value)
54+
}
55+
56+
func ContextGetNoExit(ctx context.Context) bool {
57+
value, ok := ctx.Value(noExitKey).(bool)
58+
return ok && value
59+
}
60+
61+
func ContextSetStderr(ctx context.Context, value io.Writer) context.Context {
62+
return context.WithValue(ctx, stderrKey, value)
63+
}
64+
65+
func ContextGetStderr(ctx context.Context) io.Writer {
66+
value, ok := ctx.Value(stderrKey).(io.Writer)
67+
if !ok {
68+
return os.Stderr
69+
}
70+
return value
71+
}
72+
73+
func ContextSetStdout(ctx context.Context, value io.Writer) context.Context {
74+
return context.WithValue(ctx, stdoutKey, value)
75+
}
76+
77+
func ContextGetStdout(ctx context.Context) io.Writer {
78+
value, ok := ctx.Value(stderrKey).(io.Writer)
79+
if !ok {
80+
return os.Stdout
81+
}
82+
return value
83+
}
84+
85+
func ContextSetStdin(ctx context.Context, value io.Reader) context.Context {
86+
return context.WithValue(ctx, stdinKey, value)
87+
}
88+
89+
func ContextGetStdin(ctx context.Context) io.Reader {
90+
value, ok := ctx.Value(stdinKey).(io.Reader)
91+
if !ok {
92+
return os.Stdin
93+
}
94+
return value
95+
}
96+
97+
// copied from ../cmd.go
98+
func initDB(ctx context.Context) error {
99+
setting.MustInstalled()
100+
setting.LoadDBSetting()
101+
setting.InitSQLLoggersForCli(log.INFO)
102+
103+
if setting.Database.Type == "" {
104+
log.Fatal(`Database settings are missing from the configuration file: %q.
105+
Ensure you are running in the correct environment or set the correct configuration file with -c.
106+
If this is the intended configuration file complete the [database] section.`, setting.CustomConf)
107+
}
108+
if err := db.InitEngine(ctx); err != nil {
109+
return fmt.Errorf("unable to initialize the database using the configuration in %q. Error: %w", setting.CustomConf, err)
110+
}
111+
return nil
112+
}
113+
114+
// copied from ../cmd.go
115+
func installSignals(ctx context.Context) (context.Context, context.CancelFunc) {
116+
ctx, cancel := context.WithCancel(ctx)
117+
go func() {
118+
// install notify
119+
signalChannel := make(chan os.Signal, 1)
120+
121+
signal.Notify(
122+
signalChannel,
123+
syscall.SIGINT,
124+
syscall.SIGTERM,
125+
)
126+
select {
127+
case <-signalChannel:
128+
case <-ctx.Done():
129+
}
130+
cancel()
131+
signal.Reset()
132+
}()
133+
134+
return ctx, cancel
135+
}
136+
137+
func handleCliResponseExtra(ctx context.Context, extra private.ResponseExtra) error {
138+
if false && extra.UserMsg != "" {
139+
if _, err := fmt.Fprintf(ContextGetStdout(ctx), "%s", extra.UserMsg); err != nil {
140+
panic(err)
141+
}
142+
}
143+
if ContextGetNoExit(ctx) {
144+
return extra.Error
145+
}
146+
return cli.Exit(extra.Error, 1)
147+
}

0 commit comments

Comments
 (0)