Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/cluster-agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,16 @@ var clusterAgentCmd = &cobra.Command{
Long: `The MicroK8s cluster agent is an API server that orchestrates the
lifecycle of a MicroK8s cluster.`,
Run: func(cmd *cobra.Command, args []string) {
capiPath := os.Getenv("CAPI_PATH")
if capiPath == "" {
capiPath = capiDefaultPath
}

s := snap.NewSnap(
os.Getenv("SNAP"),
os.Getenv("SNAP_DATA"),
os.Getenv("SNAP_COMMON"),
capiPath,
snap.WithRetryApplyCNI(20, 3*time.Second),
)

Expand Down
5 changes: 5 additions & 0 deletions cmd/consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package cmd

const (
capiDefaultPath = "/capi"
)
6 changes: 6 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,16 @@ var (
Short: "Apply MicroK8s configurations",
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
capiPath := os.Getenv("CAPI_PATH")
if capiPath == "" {
capiPath = capiDefaultPath
}

s := snap.NewSnap(
os.Getenv("SNAP"),
os.Getenv("SNAP_DATA"),
os.Getenv("SNAP_COMMON"),
capiPath,
)
l := k8sinit.NewLauncher(s, initPreInit)

Expand Down
6 changes: 6 additions & 0 deletions pkg/api/v2/consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package v2

const (
// CAPIAuthTokenHeader is the header used to pass the CAPI auth token.
CAPIAuthTokenHeader = "capi-auth-token"
)
4 changes: 3 additions & 1 deletion pkg/api/v2/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ func (a *API) RegisterServer(server *http.ServeMux, middleware func(f http.Handl
return
}

if rc, err := a.RemoveFromDqlite(r.Context(), req); err != nil {
token := r.Header.Get(CAPIAuthTokenHeader)

if rc, err := a.RemoveFromDqlite(r.Context(), req, token); err != nil {
httputil.Error(w, rc, fmt.Errorf("failed to remove from dqlite: %w", err))
return
}
Expand Down
10 changes: 8 additions & 2 deletions pkg/api/v2/remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ import (
// RemoveFromDqliteRequest represents a request to remove a node from the dqlite cluster.
type RemoveFromDqliteRequest struct {
// RemoveEndpoint is the endpoint of the node to remove from the dqlite cluster.
RemoveEndpoint string `json:"removeEndpoint"`
RemoveEndpoint string `json:"remove_endpoint"`
}

// RemoveFromDqlite implements the "POST /v2/dqlite/remove" endpoint and removes a node from the dqlite cluster.
func (a *API) RemoveFromDqlite(ctx context.Context, req RemoveFromDqliteRequest) (int, error) {
func (a *API) RemoveFromDqlite(ctx context.Context, req RemoveFromDqliteRequest, token string) (int, error) {
if isValid, err := a.Snap.IsCAPIAuthTokenValid(token); err != nil {
return http.StatusUnauthorized, fmt.Errorf("failed to validate CAPI auth token: %w", err)
} else if !isValid {
return http.StatusUnauthorized, fmt.Errorf("invalid CAPI auth token %q", token)
}

if err := snaputil.RemoveNodeFromDqlite(ctx, a.Snap, req.RemoveEndpoint); err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to remove node from dqlite: %w", err)
}
Expand Down
40 changes: 36 additions & 4 deletions pkg/api/v2/remove_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,55 @@ func TestRemove(t *testing.T) {
cmdErr := errors.New("failed to run command")
apiv2 := &v2.API{
Snap: &mock.Snap{
RunCommandErr: cmdErr,
RunCommandErr: cmdErr,
CAPIAuthTokenValid: true,
},
}

rc, err := apiv2.RemoveFromDqlite(context.Background(), v2.RemoveFromDqliteRequest{RemoveEndpoint: "1.1.1.1:1234"})
rc, err := apiv2.RemoveFromDqlite(context.Background(), v2.RemoveFromDqliteRequest{RemoveEndpoint: "1.1.1.1:1234"}, "token")

g := NewWithT(t)
g.Expect(err).To(MatchError(cmdErr))
g.Expect(rc).To(Equal(http.StatusInternalServerError))
})

t.Run("InvalidToken", func(t *testing.T) {
apiv2 := &v2.API{
Snap: &mock.Snap{
CAPIAuthTokenValid: false, // explicitly set to false
},
}

rc, err := apiv2.RemoveFromDqlite(context.Background(), v2.RemoveFromDqliteRequest{RemoveEndpoint: "1.1.1.1:1234"}, "token")

g := NewWithT(t)
g.Expect(err).To(HaveOccurred())
g.Expect(rc).To(Equal(http.StatusUnauthorized))
})

t.Run("TokenFileNotFound", func(t *testing.T) {
tokenErr := errors.New("token file not found")
apiv2 := &v2.API{
Snap: &mock.Snap{
CAPIAuthTokenError: tokenErr,
},
}

rc, err := apiv2.RemoveFromDqlite(context.Background(), v2.RemoveFromDqliteRequest{RemoveEndpoint: "1.1.1.1:1234"}, "token")

g := NewWithT(t)
g.Expect(err).To(MatchError(tokenErr))
g.Expect(rc).To(Equal(http.StatusUnauthorized))
})

t.Run("RemovesSuccessfully", func(t *testing.T) {
apiv2 := &v2.API{
Snap: &mock.Snap{},
Snap: &mock.Snap{
CAPIAuthTokenValid: true,
},
}

rc, err := apiv2.RemoveFromDqlite(context.Background(), v2.RemoveFromDqliteRequest{RemoveEndpoint: "1.1.1.1:1234"})
rc, err := apiv2.RemoveFromDqlite(context.Background(), v2.RemoveFromDqliteRequest{RemoveEndpoint: "1.1.1.1:1234"}, "token")

g := NewWithT(t)
g.Expect(err).ToNot(HaveOccurred())
Expand Down
5 changes: 5 additions & 0 deletions pkg/snap/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ type Snap interface {
GetSnapDataPath(parts ...string) string
// GetSnapCommonPath returns the path to a file or directory in the snap's common directory.
GetSnapCommonPath(parts ...string) string
// GetCAPIPath returns the path to a file or directory in the CAPI directory.
GetCAPIPath(parts ...string) string

// RunCommand runs a shell command.
RunCommand(ctx context.Context, commands ...string) error
Expand Down Expand Up @@ -98,6 +100,9 @@ type Snap interface {
// GetKnownToken returns the token for a known user from the known_users.csv file.
GetKnownToken(username string) (string, error)

// IsCAPIAuthTokenValid returns true if token is a valid CAPI auth token.
IsCAPIAuthTokenValid(token string) (bool, error)

// SignCertificate signs the certificate signing request, and returns the certificate in PEM format.
SignCertificate(ctx context.Context, csrPEM []byte) ([]byte, error)

Expand Down
14 changes: 14 additions & 0 deletions pkg/snap/mock/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Snap struct {
SnapDir string
SnapDataDir string
SnapCommonDir string
CAPIDir string

RunCommandCalledWith []RunCommandCall
RunCommandErr error
Expand Down Expand Up @@ -85,6 +86,9 @@ type Snap struct {
KubeletTokens map[string]string // map hostname to token
KnownTokens map[string]string // map username to token

CAPIAuthTokenValid bool
CAPIAuthTokenError error

SignCertificateCalledWith []string // string(csrPEM)
SignedCertificate string

Expand Down Expand Up @@ -116,6 +120,11 @@ func (s *Snap) GetSnapCommonPath(parts ...string) string {
return filepath.Join(append([]string{s.SnapCommonDir}, parts...)...)
}

// GetCAPIPath is a mock implementation for the snap.Snap interface.
func (s *Snap) GetCAPIPath(parts ...string) string {
return filepath.Join(append([]string{s.CAPIDir}, parts...)...)
}

// RunCommand is a mock implementation for the snap.Snap interface.
func (s *Snap) RunCommand(_ context.Context, commands ...string) error {
s.RunCommandCalledWith = append(s.RunCommandCalledWith, RunCommandCall{Commands: commands})
Expand Down Expand Up @@ -320,6 +329,11 @@ func (s *Snap) GetKnownToken(username string) (string, error) {
return "", fmt.Errorf("no known token for user %s", username)
}

// IsCAPIAuthTokenValid is a mock implementation for the snap.Snap interface.
func (s *Snap) IsCAPIAuthTokenValid(token string) (bool, error) {
return s.CAPIAuthTokenValid, s.CAPIAuthTokenError
}

// RunUpgrade is a mock implementation for the snap.Snap interface.
func (s *Snap) RunUpgrade(ctx context.Context, upgrade string, phase string) error {
s.RunUpgradeCalledWith = append(s.RunUpgradeCalledWith, fmt.Sprintf("%s %s", upgrade, phase))
Expand Down
16 changes: 15 additions & 1 deletion pkg/snap/snap.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type snap struct {
snapDir string
snapDataDir string
snapCommonDir string
capiPath string
runCommand func(context.Context, ...string) error

clusterTokensMu sync.Mutex
Expand All @@ -36,11 +37,12 @@ type snap struct {

// NewSnap creates a new interface with the MicroK8s snap.
// NewSnap accepts the $SNAP, $SNAP_DATA and $SNAP_COMMON, directories, and a number of options.
func NewSnap(snapDir, snapDataDir, snapCommonDir string, options ...func(s *snap)) Snap {
func NewSnap(snapDir, snapDataDir, snapCommonDir, capiPath string, options ...func(s *snap)) Snap {
s := &snap{
snapDir: snapDir,
snapDataDir: snapDataDir,
snapCommonDir: snapCommonDir,
capiPath: capiPath,
runCommand: util.RunCommand,
}

Expand All @@ -65,6 +67,9 @@ func (s *snap) GetSnapDataPath(parts ...string) string {
func (s *snap) GetSnapCommonPath(parts ...string) string {
return filepath.Join(append([]string{s.snapCommonDir}, parts...)...)
}
func (s *snap) GetCAPIPath(parts ...string) string {
return filepath.Join(append([]string{s.capiPath}, parts...)...)
}

func (s *snap) GetGroupName() string {
if s.isStrict() {
Expand Down Expand Up @@ -331,6 +336,15 @@ func (s *snap) GetKnownToken(username string) (string, error) {
return "", fmt.Errorf("no known token found for user %s", username)
}

// IsCAPIAuthTokenValid checks if the given CAPI auth token is valid.
func (s *snap) IsCAPIAuthTokenValid(token string) (bool, error) {
contents, err := util.ReadFile(s.GetCAPIPath("etc", "token"))
if err != nil {
return false, fmt.Errorf("failed to read token file: %w", err)
}
return strings.TrimSpace(contents) == token, nil
}

func (s *snap) SignCertificate(ctx context.Context, csrPEM []byte) ([]byte, error) {
// TODO: consider using crypto/x509 for this instead of relying on openssl commands.
// NOTE(neoaggelos): x509.CreateCertificate() has some hardcoded fields that are incompatible with MicroK8s.
Expand Down
4 changes: 2 additions & 2 deletions pkg/snap/snap_addons_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
func TestAddons(t *testing.T) {
t.Run("EnableDisable", func(t *testing.T) {
runner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata", snap.WithCommandRunner(runner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata", "", snap.WithCommandRunner(runner.Run))

s.EnableAddon(context.Background(), "dns")
s.EnableAddon(context.Background(), "dns", "10.0.0.2")
Expand All @@ -32,7 +32,7 @@ func TestAddons(t *testing.T) {

t.Run("AddRepository", func(t *testing.T) {
runner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata", snap.WithCommandRunner(runner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata", "", snap.WithCommandRunner(runner.Run))

s.AddAddonsRepository(context.Background(), "core", "/snap/microk8s/current/addons/core", "", false)
s.AddAddonsRepository(context.Background(), "core", "/snap/microk8s/current/addons/core", "", true)
Expand Down
37 changes: 37 additions & 0 deletions pkg/snap/snap_capi_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package snap_test

import (
"os"
"path/filepath"
"testing"

. "github.com/onsi/gomega"

"github.com/canonical/microk8s-cluster-agent/pkg/snap"
)

func TestCAPIAuthToken(t *testing.T) {
capiTestPath := "./capi-test"
os.RemoveAll(capiTestPath)
s := snap.NewSnap("", "", "", capiTestPath)
token := "token123"

g := NewWithT(t)

isValid, err := s.IsCAPIAuthTokenValid(token)
g.Expect(err).To(MatchError(os.ErrNotExist))
g.Expect(isValid).To(BeFalse())

capiEtc := filepath.Join(capiTestPath, "etc")
defer os.RemoveAll(capiTestPath)
g.Expect(os.MkdirAll(capiEtc, 0755)).To(Succeed())
g.Expect(os.WriteFile("./capi-test/etc/token", []byte(token), 0600)).To(Succeed())

isValid, err = s.IsCAPIAuthTokenValid("random-token")
g.Expect(err).ToNot(HaveOccurred())
g.Expect(isValid).To(BeFalse())

isValid, err = s.IsCAPIAuthTokenValid(token)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(isValid).To(BeTrue())
}
2 changes: 1 addition & 1 deletion pkg/snap/snap_containerd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestUpdateContainerdRegistryConfigs(t *testing.T) {
}
defer os.RemoveAll("testdata/args")

s := snap.NewSnap("testdata", "testdata", "testdata")
s := snap.NewSnap("testdata", "testdata", "testdata", "")

t.Run("Mirror", func(t *testing.T) {
g := NewWithT(t)
Expand Down
2 changes: 1 addition & 1 deletion pkg/snap/snap_files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestFiles(t *testing.T) {
defer os.RemoveAll(filepath.Dir(file))
}

s := snap.NewSnap("testdata", "testdata", "testdata")
s := snap.NewSnap("testdata", "testdata", "testdata", "")

for _, tc := range []struct {
name string
Expand Down
2 changes: 1 addition & 1 deletion pkg/snap/snap_images_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestImportImage(t *testing.T) {
os.Remove("testdata/arguments")
}()
mockRunner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata/common", snap.WithCommandRunner(mockRunner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata/common", "", snap.WithCommandRunner(mockRunner.Run))

g := NewWithT(t)
err := s.ImportImage(context.Background(), bytes.NewBufferString("IMAGEDATA"))
Expand Down
6 changes: 3 additions & 3 deletions pkg/snap/snap_join_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestJoinCluster(t *testing.T) {
t.Run("PropagateError", func(t *testing.T) {
g := NewWithT(t)
runner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata", snap.WithCommandRunner(runner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata", "", snap.WithCommandRunner(runner.Run))
runner.Err = fmt.Errorf("some error")

err := s.JoinCluster(context.Background(), "some-url", false)
Expand All @@ -26,7 +26,7 @@ func TestJoinCluster(t *testing.T) {
t.Run("ControlPlane", func(t *testing.T) {
g := NewWithT(t)
runner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata", snap.WithCommandRunner(runner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata", "", snap.WithCommandRunner(runner.Run))

err := s.JoinCluster(context.Background(), "10.10.10.10:25000/token/hash", false)
g.Expect(err).To(BeNil())
Expand All @@ -36,7 +36,7 @@ func TestJoinCluster(t *testing.T) {
t.Run("Worker", func(t *testing.T) {
g := NewWithT(t)
runner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata", snap.WithCommandRunner(runner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata", "", snap.WithCommandRunner(runner.Run))

err := s.JoinCluster(context.Background(), "10.10.10.10:25000/token/hash", true)
g.Expect(err).To(BeNil())
Expand Down
2 changes: 1 addition & 1 deletion pkg/snap/snap_lock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

func TestLock(t *testing.T) {
s := snap.NewSnap("testdata", "testdata", "testdata")
s := snap.NewSnap("testdata", "testdata", "testdata", "")
if err := os.MkdirAll("testdata/var/lock", 0755); err != nil {
t.Fatalf("Failed to create directory: %s", err)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/snap/snap_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

func TestServiceRestart(t *testing.T) {
mockRunner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata", snap.WithCommandRunner(mockRunner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata", "", snap.WithCommandRunner(mockRunner.Run))

t.Run("NoKubelite", func(t *testing.T) {
for _, tc := range []struct {
Expand Down
2 changes: 1 addition & 1 deletion pkg/snap/snap_sign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestSignCertificate(t *testing.T) {
os.Remove("testdata/arguments")
}()
mockRunner := &utiltest.MockRunner{}
s := snap.NewSnap("testdata", "testdata", "testdata", snap.WithCommandRunner(mockRunner.Run))
s := snap.NewSnap("testdata", "testdata", "testdata", "", snap.WithCommandRunner(mockRunner.Run))

g := NewWithT(t)
b, err := s.SignCertificate(context.Background(), []byte("MOCK CSR"))
Expand Down
Loading
Loading