Skip to content

Commit af82cea

Browse files
feat: introduces local artifact server and materialize subcommand (#443)
* feat: introduces local artifact server and materialise - implement and install a localhost http server for local assets. - implement a hidden 'materialise' command to materialise the embedded assets into a directory. * chore: add e2e tests * chore: allow binary to be overwritten * feat: starts local artifact mirror on join * feat: block materialise for regular users * chore: add path to help message * chore: standardize on 'materialized' * chore: materialise directly to /var/lib/embedded-cluster * chore: adding some more tests * chore: add host collector/analyser
1 parent fdd2134 commit af82cea

File tree

17 files changed

+333
-16
lines changed

17 files changed

+333
-16
lines changed

.github/workflows/pull-request.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ jobs:
134134
- TestResetAndReinstall
135135
- TestCollectSupportBundle
136136
- TestOldVersionUpgrade
137+
- TestMaterialize
138+
- TestLocalArtifactMirror
137139
steps:
138140
- name: Checkout
139141
uses: actions/checkout@v4

.github/workflows/release-dev.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ jobs:
112112
- TestResetAndReinstall
113113
- TestCollectSupportBundle
114114
- TestOldVersionUpgrade
115+
- TestMaterialize
116+
- TestLocalArtifactMirror
115117
steps:
116118
- name: Checkout
117119
uses: actions/checkout@v4

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ pkg/goods/bins/kubectl-preflight: Makefile
6969
mv output/tmp/preflight/preflight pkg/goods/bins/kubectl-preflight
7070
touch pkg/goods/bins/kubectl-preflight
7171

72+
pkg/goods/bins/local-artifact-mirror: Makefile
73+
mkdir -p pkg/goods/bins
74+
CGO_ENABLED=0 go build -o pkg/goods/bins/local-artifact-mirror ./cmd/local-artifact-mirror
75+
7276
output/tmp/release.tar.gz: e2e/kots-release-install/*
7377
mkdir -p output/tmp
7478
tar -czf output/tmp/release.tar.gz -C e2e/kots-release-install .
@@ -89,7 +93,8 @@ go.mod: Makefile
8993
static: pkg/goods/bins/k0s \
9094
pkg/goods/bins/kubectl-preflight \
9195
pkg/goods/bins/kubectl \
92-
pkg/goods/bins/kubectl-support_bundle
96+
pkg/goods/bins/kubectl-support_bundle \
97+
pkg/goods/bins/local-artifact-mirror
9398

9499
.PHONY: embedded-cluster-linux-amd64
95100
embedded-cluster-linux-amd64: static go.mod

cmd/embedded-cluster/install.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package main
22

33
import (
44
"bytes"
5-
"context"
65
"fmt"
76
"os"
87
"os/exec"
@@ -52,6 +51,30 @@ func runCommand(bin string, args ...string) (string, error) {
5251
return stdout.String(), nil
5352
}
5453

54+
// installAndEnableLocalArtifactMirror installs and enables the local artifact mirror. This
55+
// service is reponsible for serving on localhost, through http, all files that are used
56+
// during a cluster upgrade.
57+
func installAndEnableLocalArtifactMirror() error {
58+
ourbin := defaults.PathToEmbeddedClusterBinary("local-artifact-mirror")
59+
hstbin := defaults.LocalArtifactMirrorPath()
60+
if err := os.Rename(ourbin, hstbin); err != nil {
61+
return fmt.Errorf("unable to move local artifact mirror binary: %w", err)
62+
}
63+
if err := goods.MaterializeLocalArtifactMirrorUnitFile(); err != nil {
64+
return fmt.Errorf("failed to materialize artifact mirror unit: %w", err)
65+
}
66+
if _, err := runCommand("systemctl", "daemon-reload"); err != nil {
67+
return fmt.Errorf("unable to get reload systemctl daemon: %w", err)
68+
}
69+
if _, err := runCommand("systemctl", "start", "local-artifact-mirror"); err != nil {
70+
return fmt.Errorf("unable to start the local artifact mirror: %w", err)
71+
}
72+
if _, err := runCommand("systemctl", "enable", "local-artifact-mirror"); err != nil {
73+
return fmt.Errorf("unable to start the local artifact mirror: %w", err)
74+
}
75+
return nil
76+
}
77+
5578
// runPostInstall is a helper function that run things just after the k0s install
5679
// command ran.
5780
func runPostInstall() error {
@@ -63,7 +86,7 @@ func runPostInstall() error {
6386
if _, err := runCommand("systemctl", "daemon-reload"); err != nil {
6487
return fmt.Errorf("unable to get reload systemctl daemon: %w", err)
6588
}
66-
return nil
89+
return installAndEnableLocalArtifactMirror()
6790
}
6891

6992
// runHostPreflights run the host preflights we found embedded in the binary
@@ -232,7 +255,7 @@ func applyUnsupportedOverrides(c *cli.Context, cfg *k0sconfig.ClusterConfig) (*k
232255

233256
// installK0s runs the k0s install command and waits for it to finish. If no configuration
234257
// is found one is generated.
235-
func installK0s(c *cli.Context) error {
258+
func installK0s() error {
236259
ourbin := defaults.PathToEmbeddedClusterBinary("k0s")
237260
hstbin := defaults.K0sBinaryPath()
238261
if err := os.Rename(ourbin, hstbin); err != nil {
@@ -249,7 +272,7 @@ func installK0s(c *cli.Context) error {
249272

250273
// waitForK0s waits for the k0s API to be available. We wait for the k0s socket to
251274
// appear in the system and until the k0s status command to finish.
252-
func waitForK0s(ctx context.Context) error {
275+
func waitForK0s() error {
253276
loading := spinner.Start()
254277
defer loading.Close()
255278
loading.Infof("Waiting for %s node to be ready", defaults.BinaryName())
@@ -361,7 +384,7 @@ var installCommand = &cli.Command{
361384
return err
362385
}
363386
logrus.Debugf("installing k0s")
364-
if err := installK0s(c); err != nil {
387+
if err := installK0s(); err != nil {
365388
err := fmt.Errorf("unable update cluster: %w", err)
366389
metrics.ReportApplyFinished(c, err)
367390
return err
@@ -373,7 +396,7 @@ var installCommand = &cli.Command{
373396
return err
374397
}
375398
logrus.Debugf("waiting for k0s to be ready")
376-
if err := waitForK0s(c.Context); err != nil {
399+
if err := waitForK0s(); err != nil {
377400
err := fmt.Errorf("unable to wait for node: %w", err)
378401
metrics.ReportApplyFinished(c, err)
379402
return err

cmd/embedded-cluster/join.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,15 @@ var joinCommand = &cli.Command{
161161
}
162162

163163
logrus.Infof("Applying configuration overrides")
164-
if err := applyJoinConfigurationOverrides(c, jcmd); err != nil {
164+
if err := applyJoinConfigurationOverrides(jcmd); err != nil {
165165
err := fmt.Errorf("unable to apply configuration overrides: %w", err)
166166
metrics.ReportJoinFailed(c.Context, jcmd.MetricsBaseURL, jcmd.ClusterID, err)
167167
return err
168168
}
169169

170-
logrus.Infof("Creating systemd unit file")
171-
if err := createSystemdUnitFile(jcmd.K0sJoinCommand); err != nil {
172-
err := fmt.Errorf("unable to create systemd unit file: %w", err)
170+
logrus.Infof("Creating systemd unit files")
171+
if err := createSystemdUnitFiles(jcmd.K0sJoinCommand); err != nil {
172+
err := fmt.Errorf("unable to create systemd unit files: %w", err)
173173
metrics.ReportJoinFailed(c.Context, jcmd.MetricsBaseURL, jcmd.ClusterID, err)
174174
return err
175175
}
@@ -189,7 +189,7 @@ var joinCommand = &cli.Command{
189189

190190
// applyJoinConfigurationOverrides applies both config overrides received from the kots api.
191191
// Applies first the EmbeddedOverrides and then the EndUserOverrides.
192-
func applyJoinConfigurationOverrides(c *cli.Context, jcmd *JoinCommandResponse) error {
192+
func applyJoinConfigurationOverrides(jcmd *JoinCommandResponse) error {
193193
patch, err := jcmd.EmbeddedOverrides()
194194
if err != nil {
195195
return fmt.Errorf("unable to get embedded overrides: %w", err)
@@ -297,8 +297,9 @@ func startK0sService() error {
297297
return nil
298298
}
299299

300-
// createSystemdUnitFile links the k0s systemd unit file.
301-
func createSystemdUnitFile(fullcmd string) error {
300+
// createSystemdUnitFiles links the k0s systemd unit file. this also creates a new
301+
// systemd unit file for the local artifact mirror service.
302+
func createSystemdUnitFiles(fullcmd string) error {
302303
dst := fmt.Sprintf("/etc/systemd/system/%s.service", defaults.BinaryName())
303304
if _, err := os.Stat(dst); err == nil {
304305
if err := os.Remove(dst); err != nil {
@@ -315,7 +316,7 @@ func createSystemdUnitFile(fullcmd string) error {
315316
if _, err := runCommand("systemctl", "daemon-reload"); err != nil {
316317
return err
317318
}
318-
return nil
319+
return installAndEnableLocalArtifactMirror()
319320
}
320321

321322
// runK0sInstallCommand runs the k0s install command as provided by the kots

cmd/embedded-cluster/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func main() {
3333
versionCommand,
3434
joinCommand,
3535
resetCommand,
36+
materializeCommand,
3637
},
3738
}
3839
if err := app.RunContext(ctx, os.Args); err != nil {

cmd/embedded-cluster/materialize.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/urfave/cli/v2"
8+
9+
"github.com/replicatedhq/embedded-cluster/pkg/goods"
10+
)
11+
12+
var materializeCommand = &cli.Command{
13+
Name: "materialize",
14+
Usage: "Materialize embedded assets on /var/lib/embedded-cluster",
15+
Hidden: true,
16+
Before: func(c *cli.Context) error {
17+
if os.Getuid() != 0 {
18+
return fmt.Errorf("materialize command must be run as root")
19+
}
20+
return nil
21+
},
22+
Action: func(c *cli.Context) error {
23+
if err := goods.Materialize(); err != nil {
24+
return fmt.Errorf("unable to materialize: %v", err)
25+
}
26+
return nil
27+
},
28+
}

cmd/embedded-cluster/uninstall.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,19 @@ var resetCommand = &cli.Command{
385385
}
386386
}
387387

388+
lamPath := "/etc/systemd/system/local-artifact-mirror.service"
389+
if _, err := os.Stat(lamPath); err == nil {
390+
if _, err := runCommand("systemctl", "stop", "local-artifact-mirror"); err != nil {
391+
return err
392+
}
393+
if err := os.RemoveAll(lamPath); err != nil {
394+
return err
395+
}
396+
if err := os.RemoveAll(defaults.LocalArtifactMirrorPath()); err != nil {
397+
return err
398+
}
399+
}
400+
388401
if _, err := os.Stat(defaults.EmbeddedClusterHomeDirectory()); err == nil {
389402
if err := os.RemoveAll(defaults.EmbeddedClusterHomeDirectory()); err != nil {
390403
return err

cmd/local-artifact-mirror/main.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"os"
8+
"os/signal"
9+
"path"
10+
"strings"
11+
"syscall"
12+
"time"
13+
14+
"github.com/sirupsen/logrus"
15+
"github.com/urfave/cli/v2"
16+
17+
"github.com/replicatedhq/embedded-cluster/pkg/defaults"
18+
)
19+
20+
func main() {
21+
ctx, cancel := signal.NotifyContext(
22+
context.Background(),
23+
syscall.SIGINT,
24+
syscall.SIGTERM,
25+
)
26+
defer cancel()
27+
name := path.Base(os.Args[0])
28+
var app = &cli.App{
29+
Name: name,
30+
Usage: "Runs the local artifact mirror",
31+
Commands: []*cli.Command{serveCommand},
32+
}
33+
if err := app.RunContext(ctx, os.Args); err != nil {
34+
logrus.Fatal(err)
35+
}
36+
}
37+
38+
// logAndFilterRequest is a middleware that logs the HTTP request details. Returns 404
39+
// if attempting to read the log files as those are not served by this server.
40+
func logAndFilterRequest(handler http.Handler) http.Handler {
41+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
42+
fmt.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL)
43+
if strings.HasPrefix(r.URL.Path, "/logs") {
44+
w.WriteHeader(http.StatusNotFound)
45+
return
46+
}
47+
handler.ServeHTTP(w, r)
48+
})
49+
}
50+
51+
// serveCommand starts a http server that serves files from the /var/lib/embedded-cluster
52+
// directory. This servers listen only on localhost and is used to serve files needed by
53+
// the autopilot during an upgrade.
54+
var serveCommand = &cli.Command{
55+
Name: "serve",
56+
Usage: "Serve /var/lib/embedded-cluster files over HTTP",
57+
Before: func(c *cli.Context) error {
58+
if os.Getuid() != 0 {
59+
return fmt.Errorf("serve command must be run as root")
60+
}
61+
return nil
62+
},
63+
Action: func(c *cli.Context) error {
64+
dir := defaults.EmbeddedClusterHomeDirectory()
65+
66+
fileServer := http.FileServer(http.Dir(dir))
67+
loggedFileServer := logAndFilterRequest(fileServer)
68+
http.Handle("/", loggedFileServer)
69+
70+
stop := make(chan os.Signal, 1)
71+
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
72+
73+
server := &http.Server{Addr: "127.0.0.1:50000"}
74+
go func() {
75+
fmt.Println("Starting server on 127.0.0.1:50000")
76+
if err := server.ListenAndServe(); err != nil {
77+
if err != http.ErrServerClosed {
78+
panic(err)
79+
}
80+
}
81+
}()
82+
83+
<-stop
84+
fmt.Println("Shutting down server...")
85+
86+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
87+
defer cancel()
88+
if err := server.Shutdown(ctx); err != nil {
89+
panic(err)
90+
}
91+
fmt.Println("Server gracefully stopped")
92+
return nil
93+
},
94+
}

e2e/local-artifact-mirror_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package e2e
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/replicatedhq/embedded-cluster/e2e/cluster"
8+
)
9+
10+
func TestLocalArtifactMirror(t *testing.T) {
11+
t.Parallel()
12+
tc := cluster.NewTestCluster(&cluster.Input{
13+
T: t,
14+
Nodes: 1,
15+
Image: "ubuntu/jammy",
16+
EmbeddedClusterPath: "../output/bin/embedded-cluster-original",
17+
})
18+
defer tc.Destroy()
19+
20+
t.Logf("%s: installing embedded-cluster on node 0", time.Now().Format(time.RFC3339))
21+
line := []string{"default-install.sh"}
22+
stdout, stderr, err := RunCommandOnNode(t, tc, 0, line)
23+
if err != nil {
24+
t.Log("stdout:", stdout)
25+
t.Log("stderr:", stderr)
26+
t.Fatalf("fail to install embedded-cluster on node %s: %v", tc.Nodes[0], err)
27+
}
28+
29+
commands := [][]string{
30+
{"apt-get", "install", "curl", "-y"},
31+
{"systemctl", "status", "local-artifact-mirror"},
32+
{"systemctl", "stop", "local-artifact-mirror"},
33+
{"systemctl", "start", "local-artifact-mirror"},
34+
{"systemctl", "status", "local-artifact-mirror"},
35+
{"curl", "-o", "/tmp/kubectl-test", "127.0.0.1:50000/bin/kubectl"},
36+
{"chmod", "755", "/tmp/kubectl-test"},
37+
{"/tmp/kubectl-test", "version", "--client"},
38+
}
39+
if err := RunCommandsOnNode(t, tc, 0, commands); err != nil {
40+
t.Fatalf("fail testing local artifact mirror: %v", err)
41+
}
42+
43+
command := []string{"cp", "/etc/passwd", "/var/lib/embedded-cluster/logs/passwd"}
44+
if stdout, stderr, err := RunCommandOnNode(t, tc, 0, command); err != nil {
45+
t.Log("stdout:", stdout)
46+
t.Log("stderr:", stderr)
47+
t.Fatalf("fail to copy file: %v", err)
48+
}
49+
50+
command = []string{"curl", "-O", "--fail", "127.0.0.1:50000/logs/passwd"}
51+
t.Logf("running %v", command)
52+
if _, _, err := RunCommandOnNode(t, tc, 0, command); err == nil {
53+
t.Fatalf("we should not be able to fetch logs from local artifact mirror")
54+
}
55+
56+
command = []string{"curl", "-O", "--fail", "127.0.0.1:50000/../../../etc/passwd"}
57+
t.Logf("running %v", command)
58+
if _, _, err := RunCommandOnNode(t, tc, 0, command); err == nil {
59+
t.Fatalf("we should not be able to fetch paths with ../")
60+
}
61+
62+
t.Logf("%s: test complete", time.Now().Format(time.RFC3339))
63+
}

0 commit comments

Comments
 (0)