Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 10 additions & 1 deletion cmd/cli/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,17 @@ func depsCommand() *cli.Command {
cmd := &cli.Command{
Name: "deps",
Usage: "shows status of external dependencies",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "runner",
Destination: &runner,
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
return deps.Run()
cfg := profileConfigOrDefault("")
return deps.Run(deps.WithConfig(cfg),
deps.WithRunner(runner),
)
},
}
return cmd
Expand Down
66 changes: 66 additions & 0 deletions cmd/cli/flatpak.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package cli

import (
"context"

"github.com/qubesome/cli/internal/flatpak"
"github.com/qubesome/cli/internal/inception"
"github.com/qubesome/cli/internal/types"
"github.com/urfave/cli/v3"
)

func flatpakCommand() *cli.Command {
cmd := &cli.Command{
Name: "flatpak",
Commands: []*cli.Command{
{
Name: "run",
Usage: "execute Flatpak workloads from the host into a Qubesome profile",
Description: `Examples:

qubesome flatpak run org.kde.francis - Run the org.kde.francis flatpak on the active profile
qubesome flatpak run -profile <profile> org.kde.francis - Run the org.kde.francis flatpak on a specific profile
`,
Arguments: []cli.Argument{
&cli.StringArg{
Name: "workload",
Min: 1,
Max: 1,
Destination: &workload,
},
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "profile",
Destination: &targetProfile,
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
var cfg *types.Config

// Commands that can be executed from within a profile
// (a.k.a. inception mode) should not check for profile
// names nor configs, as those are imposed by the inception
// server.
if !inception.Inside() {
prof, err := profileOrActive(targetProfile)
if err != nil {
return err
}

targetProfile = prof.Name
cfg = profileConfigOrDefault(targetProfile)
}

return flatpak.Run(
flatpak.WithName(workload),
flatpak.WithProfile(targetProfile),
flatpak.WithConfig(cfg),
flatpak.WithExtraArgs(cmd.Args().Slice()),
)
},
},
},
}
return cmd
}
1 change: 1 addition & 0 deletions cmd/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func RootCommand() *cli.Command {
versionCommand(),
completionCommand(),
hostRunCommand(),
flatpakCommand(),
},
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/cli/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ qubesome start -git https://github.com/qubesome/sample-dotfiles i3
// so that the containerised Windows Manager is able to execute
// new container workloads. This self-calls qubesome and leave it
// running so that the main process exits right away.
if !selfcall {
if !debug && !selfcall {
cmd := exec.Command(os.Args[0], os.Args[1:]...) //nolint
cmd.Env = append(cmd.Env, "QUBESOME_SELFCALL=true")
cmd.Env = append(cmd.Env, os.Environ()...)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/qubesome/cli

go 1.23.3
go 1.24.0

require (
github.com/cyphar/filepath-securejoin v0.4.1
Expand Down
4 changes: 2 additions & 2 deletions hack/base.mk
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
GOLANGCI_VERSION ?= v1.62.0
PROTOC_VERSION ?= 28.3
GOLANGCI_VERSION ?= v1.64.5
PROTOC_VERSION ?= 29.3
TOOLS_BIN := $(shell mkdir -p build/tools && realpath build/tools)

ifneq ($(shell git status --porcelain --untracked-files=no),)
Expand Down
30 changes: 29 additions & 1 deletion internal/deps/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/qubesome/cli/internal/command"
"github.com/qubesome/cli/internal/files"
"github.com/qubesome/cli/internal/images"
)

var (
Expand Down Expand Up @@ -60,7 +61,12 @@ var optionalDeps map[string][]string = map[string][]string{
},
}

func Run(_ ...command.Option[interface{}]) error {
func Run(opts ...command.Option[Options]) error {
o := &Options{}
for _, opt := range opts {
opt(o)
}

writer := tabwriter.NewWriter(os.Stdout, 0, 0, 5, ' ', 0)
fmt.Fprintln(writer, "Command\tDependency\tStatus")
fmt.Fprintln(writer, "-------\t----------\t------")
Expand Down Expand Up @@ -88,6 +94,28 @@ func Run(_ ...command.Option[interface{}]) error {
}
}
}
writer.Flush()
fmt.Println()

if o.Config == nil {
fmt.Println("images not checked: qubesome config not found")
return nil
}

bin := files.ContainerRunnerBinary(o.Runner)
imgs, err := images.MissingImages(bin, o.Config)
if err != nil {
return err
}

writer = tabwriter.NewWriter(os.Stdout, 0, 0, 5, ' ', 0)
fmt.Fprintln(writer, "Image\tRunner\tStatus")
fmt.Fprintln(writer, "-------\t----------\t------")
for _, img := range imgs {
status := amber + "Missing" + reset

fmt.Fprintf(writer, "%s\t%s\t%s\n", img, bin, status)
}

writer.Flush()

Expand Down
23 changes: 23 additions & 0 deletions internal/deps/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package deps

import (
"github.com/qubesome/cli/internal/command"
"github.com/qubesome/cli/internal/types"
)

type Options struct {
Config *types.Config
Runner string
}

func WithConfig(cfg *types.Config) command.Option[Options] {
return func(o *Options) {
o.Config = cfg
}
}

func WithRunner(runner string) command.Option[Options] {
return func(o *Options) {
o.Runner = runner
}
}
8 changes: 8 additions & 0 deletions internal/files/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,11 @@ func GitDirPath(url string) (string, error) {
func WorkloadsDir(root, path string) (string, error) {
return securejoin.SecureJoin(root, filepath.Join(path, "workloads"))
}

func FlatpakApps() string {
return "/var/lib/flatpak/exports/share/applications"
}

func FlatpakIcons() string {
return "/var/lib/flatpak/exports/share/icons/hicolor/scalable/apps"
}
59 changes: 59 additions & 0 deletions internal/flatpak/flatpak.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package flatpak

import (
"context"
"fmt"
"os"
"os/exec"

"github.com/qubesome/cli/internal/command"
"github.com/qubesome/cli/internal/files"
"github.com/qubesome/cli/internal/inception"
)

func Run(opts ...command.Option[Options]) error {
o := &Options{}
for _, opt := range opts {
opt(o)
}

if err := o.Validate(); err != nil {
return err
}

if inception.Inside() {
client := inception.NewClient(files.InProfileSocketPath())
return client.FlatpakRun(context.TODO(), o.Name, o.ExtraArgs)
}

if o.Config == nil {
return fmt.Errorf("no config found")
}

prof, ok := o.Config.Profiles[o.Profile]
if !ok {
return fmt.Errorf("cannot find profile %q", o.Profile)
}

allowed := false
for _, name := range prof.Flatpaks {
if name == o.Name {
allowed = true
break
}
}

if !allowed {
return fmt.Errorf("flatpak %q is not allowed for profile %q", o.Name, o.Profile)
}

args := []string{"run", o.Name}
args = append(args, o.ExtraArgs...)

c := exec.Command("/usr/bin/flatpak", args...)
c.Env = append(os.Environ(), fmt.Sprintf("DISPLAY=:%d", prof.Display))
out, err := c.CombinedOutput()
fmt.Println(string(out))

return err
}
54 changes: 54 additions & 0 deletions internal/flatpak/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package flatpak

import (
"fmt"
"regexp"

"github.com/qubesome/cli/internal/command"
"github.com/qubesome/cli/internal/types"
)

var flatpakRegex = regexp.MustCompile(`^[a-z0-9]+(\.[a-z0-9]+)+$`)

type Options struct {
Name string
Config *types.Config
Profile string
ExtraArgs []string
}

func WithExtraArgs(args []string) command.Option[Options] {
return func(o *Options) {
o.ExtraArgs = args
}
}

func WithProfile(profile string) command.Option[Options] {
return func(o *Options) {
o.Profile = profile
}
}

func WithName(name string) command.Option[Options] {
return func(o *Options) {
o.Name = name
}
}

func WithConfig(cfg *types.Config) command.Option[Options] {
return func(o *Options) {
o.Config = cfg
}
}

func (o *Options) Validate() error {
if o.Name == "" {
return fmt.Errorf("missing flatpak name")
}

if !flatpakRegex.MatchString(o.Name) {
return fmt.Errorf("invalid flatpak name: %q", o.Name)
}

return nil
}
68 changes: 68 additions & 0 deletions internal/flatpak/options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package flatpak_test

import (
"testing"

"github.com/qubesome/cli/internal/flatpak"
"github.com/qubesome/cli/internal/types"
"github.com/stretchr/testify/assert"
)

func TestValidate(t *testing.T) {
tests := []struct {
name string
opts flatpak.Options
wantErr bool
errMsg string
}{
{
name: "empty flatpak",
opts: flatpak.Options{},
wantErr: true,
errMsg: "missing flatpak name",
},
{
name: "flatpak name: ../",
opts: flatpak.Options{
Name: "../",
},
wantErr: true,
errMsg: "invalid flatpak name",
},
{
name: "flatpak name: foo..ball",
opts: flatpak.Options{
Name: "foo..ball",
},
wantErr: true,
errMsg: "invalid flatpak name",
},
{
name: "flatpak name: foo../ball",
opts: flatpak.Options{
Name: "foo../ball",
},
wantErr: true,
errMsg: "invalid flatpak name",
},
{
name: "flatpak name: foo.bar.ball",
opts: flatpak.Options{
Name: "foo.bar.ball",
Config: &types.Config{},
},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.opts.Validate()

if tc.wantErr {
assert.ErrorContains(t, err, tc.errMsg)
} else {
assert.NoError(t, err)
}
})
}
}
Loading