Skip to content

Commit e16953d

Browse files
committed
feat!: Multiple improvements
1 parent 2f4bd18 commit e16953d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+7190
-3635
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
*.dll
88
*.so
99
*.dylib
10-
fireactions
1110

1211
# Test binary, built with `go test -c`
1312
*.test

.golangci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version: "2"
2+
3+
linters:
4+
default: all
5+
disable:
6+
- errcheck

Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,11 @@ fmt:
2424
.PHONY: test
2525
test:
2626
@ $(GO) test -v ./...
27+
28+
.PHONY: proto
29+
proto:
30+
@ buf generate
31+
32+
.PHONY: proto-lint
33+
proto-lint:
34+
@ buf lint

README.md

Lines changed: 29 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -42,65 +42,42 @@ Several key features:
4242

4343
## Quickstart
4444

45-
Create and install a GitHub App (see [Creating a GitHub App](https://docs.github.com/en/developers/apps/creating-a-github-app)) with the following permissions:
45+
```bash
46+
$ fireactions --help
47+
BYOM (Bring Your Own Metal) and run self-hosted GitHub runners in ephemeral, fast and secure Firecracker based virtual machines.
4648

47-
- Read access to metadata
48-
- Read and write access to actions and organization self hosted runners
49+
Usage:
50+
fireactions [command]
4951

50-
Note down the GitHub App ID and generate a private key, save it to a file on the host machine, e.g. `/root/private-key.pem`.
52+
Main application commands:
53+
server Starts the server
54+
agent Starts the agent and GitHub Actions runner inside the VM
5155

52-
Download and run the installation script:
56+
Pool management commands:
57+
pools Manage pools
5358

54-
```bash
55-
curl -sSL https://raw.githubusercontent.com/hostinger/fireactions/main/install.sh -o install.sh
56-
chmod +x install.sh
57-
./install.sh --help
58-
This script installs Fireactions on a Linux machine.
59-
60-
Usage: ./install.sh [options]
61-
62-
Options:
63-
--github-app-id Sepcify the ID of the GitHub App (required)
64-
--github-app-key-file Specify the path to the GitHub App private key file (required)
65-
--github-organization Specify the name of the GitHub organization (required)
66-
--fireactions-version Specify the Fireactions version to install (default: 0.2.5)
67-
--firecracker-version Specify the Firecracker version to install (default: 1.4.1)
68-
--kernel-version Specify the kernel version to install (default: 5.10)
69-
--containerd-snapshotter-device Specify the device to use for Containerd snapshot storage (required)
70-
--containerd-version Specify the Containerd version to install (default: 1.7.0)
71-
--cni-version Specify the CNI plugin version to install (default: 1.6.0)
72-
-h, --help Show this help message
73-
```
59+
Machine management commands:
60+
ps List all running machines across all pools
61+
login SSH into a running VM as root user
62+
logs Stream logs from the fireactions-agent service inside a machine
63+
64+
Image management commands:
65+
image Manage images
7466

75-
This creates a default configuration with a single pool named `default` with a single runner. See [Configuration](./docs/user-guide/configuration.md) for more information.
76-
77-
Test the installation by creating a new GitHub workflow in your repository:
78-
79-
```yaml
80-
# .github/workflows/test.yaml
81-
name: test
82-
83-
on:
84-
workflow_dispatch:
85-
pull_request:
86-
branches:
87-
- '*'
88-
push:
89-
branches:
90-
- main
91-
92-
jobs:
93-
test:
94-
name: test
95-
runs-on: # The label(s) of the Fireactions pool
96-
- self-hosted
97-
- fireactions
98-
steps:
99-
- name: Example
100-
run: |
101-
echo "Hello, Fireactions!"
67+
Additional Commands:
68+
version Show version information
69+
help Help about any command
70+
completion Generate the autocompletion script for the specified shell
71+
72+
Flags:
73+
-h, --help help for fireactions
74+
-v, --version version for fireactions
75+
76+
Use "fireactions [command] --help" for more information about a command.
10277
```
10378

79+
See the [User Guide](https://fireactions.io/user-guide/) for installation and configuration instructions.
80+
10481
## Contributing
10582

10683
See [CONTRIBUTING.md](CONTRIBUTING.md) for more information on how to contribute to Fireactions.

agent/agent.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package agent
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"time"
11+
12+
"github.com/firecracker-microvm/firecracker-go-sdk/vsock"
13+
"github.com/hostinger/fireactions/agent/runner"
14+
agentv1 "github.com/hostinger/fireactions/proto/agent/v1"
15+
"github.com/rs/zerolog"
16+
"github.com/sirupsen/logrus"
17+
"golang.org/x/sys/unix"
18+
"google.golang.org/grpc"
19+
)
20+
21+
const (
22+
logFilePath = "/var/log/fireactions-agent.log"
23+
)
24+
25+
type Agent struct {
26+
agentv1.UnimplementedAgentServiceServer
27+
cfg Config
28+
logFile string
29+
logFileWriter *os.File
30+
logger *zerolog.Logger
31+
runner *runner.Runner
32+
}
33+
34+
type Opt func(a *Agent)
35+
36+
func New(cfg Config, opts ...Opt) (*Agent, error) {
37+
if cfgErr := cfg.Validate(); cfgErr != nil {
38+
return nil, fmt.Errorf("validate config: %w", cfgErr)
39+
}
40+
41+
a := &Agent{
42+
cfg: cfg,
43+
logFile: logFilePath,
44+
}
45+
46+
for _, opt := range opts {
47+
opt(a)
48+
}
49+
50+
if err := a.setupLogger(); err != nil {
51+
return nil, err
52+
}
53+
54+
return a, nil
55+
}
56+
57+
func (a *Agent) setupLogger() error {
58+
logDir := filepath.Dir(a.logFile)
59+
if err := os.MkdirAll(logDir, 0755); err != nil {
60+
return fmt.Errorf("create log directory: %w", err)
61+
}
62+
63+
logFileWriter, err := os.OpenFile(a.logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
64+
if err != nil {
65+
return fmt.Errorf("open log file: %w", err)
66+
}
67+
68+
a.logFileWriter = logFileWriter
69+
70+
logLevel, err := zerolog.ParseLevel(a.cfg.LogLevel)
71+
if err != nil {
72+
return fmt.Errorf("parse log level: %w", err)
73+
}
74+
75+
multiWriter := io.MultiWriter(os.Stdout, logFileWriter)
76+
logger := zerolog.New(zerolog.ConsoleWriter{Out: multiWriter, TimeFormat: time.RFC3339}).With().
77+
Timestamp().
78+
Logger().Level(logLevel)
79+
80+
a.logger = &logger
81+
return nil
82+
}
83+
84+
// Close closes the agent resources, including the log file.
85+
func (a *Agent) Close() error {
86+
if a.logFileWriter != nil {
87+
return a.logFileWriter.Close()
88+
}
89+
90+
return nil
91+
}
92+
93+
func (a *Agent) Run(ctx context.Context) error {
94+
if err := a.setHostname(); err != nil {
95+
return fmt.Errorf("setting hostname: %w", err)
96+
}
97+
98+
// Run GitHub runner in background - it will trigger shutdown on success
99+
go a.runGitHubRunner(ctx)
100+
101+
// Run gRPC server in main flow
102+
return a.runGRPCServer(ctx)
103+
}
104+
105+
func (a *Agent) runGRPCServer(ctx context.Context) error {
106+
logrusLogger := logrus.New()
107+
logrusLogger.SetLevel(logrus.InfoLevel)
108+
logrusEntry := logrus.NewEntry(logrusLogger)
109+
110+
listener, err := vsock.Listener(ctx, logrusEntry, a.cfg.Port)
111+
if err != nil {
112+
return fmt.Errorf("vsock listen: %w", err)
113+
}
114+
defer listener.Close()
115+
116+
grpcServer := grpc.NewServer()
117+
agentv1.RegisterAgentServiceServer(grpcServer, a)
118+
119+
errCh := make(chan error, 1)
120+
go func() {
121+
a.logger.Info().Msgf("Agent GRPC server listening on VSOCK port %d", a.cfg.Port)
122+
if err := grpcServer.Serve(listener); err != nil {
123+
errCh <- fmt.Errorf("grpc serve: %w", err)
124+
}
125+
}()
126+
127+
select {
128+
case <-ctx.Done():
129+
grpcServer.GracefulStop()
130+
return nil
131+
case err := <-errCh:
132+
return err
133+
}
134+
}
135+
136+
func (a *Agent) runGitHubRunner(ctx context.Context) {
137+
a.runner = runner.New(
138+
a.cfg.RunnerJITConfig,
139+
runner.WithLogger(a.logger),
140+
)
141+
142+
if err := a.runner.Run(ctx); err != nil {
143+
a.logger.Error().Err(err).Msg("Runner encountered an error")
144+
}
145+
146+
if !a.cfg.ShutdownOnExit {
147+
a.logger.Info().Msg("Runner completed, but shutdown on exit is disabled - keeping VM running")
148+
return
149+
}
150+
151+
a.logger.Info().Msg("Runner completed, initiating VM shutdown")
152+
a.shutdown()
153+
}
154+
155+
func (a *Agent) shutdown() {
156+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
157+
defer cancel()
158+
159+
cmd := exec.CommandContext(ctx, "systemctl", "reboot")
160+
if err := cmd.Run(); err != nil {
161+
a.logger.Error().Err(err).Msg("Failed to initiate VM shutdown")
162+
}
163+
164+
a.logger.Info().Msg("Shutdown command executed")
165+
}
166+
167+
func (a *Agent) setHostname() error {
168+
if err := unix.Sethostname([]byte(a.cfg.Hostname)); err != nil {
169+
return fmt.Errorf("sethostname: %w", err)
170+
}
171+
172+
return nil
173+
}

agent/agent_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package agent

agent/config.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package agent
2+
3+
import "github.com/go-playground/validator/v10"
4+
5+
type Config struct {
6+
Port uint32 `validate:"required"`
7+
RunnerJITConfig string `validate:"required"`
8+
Hostname string `validate:"required"`
9+
LogLevel string `validate:"required,oneof=debug info warn error fatal panic trace"`
10+
ShutdownOnExit bool `validate:""`
11+
}
12+
13+
func (c Config) Validate() error {
14+
return validator.New().Struct(c)
15+
}

agent/config_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package agent

0 commit comments

Comments
 (0)