Skip to content
Closed
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
37 changes: 22 additions & 15 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,43 @@
"protocol": "http"
}
},
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"ghcr.io/devcontainers/features/node:2": {},
"ghcr.io/devcontainers/features/go:1": {},
"ghcr.io/thediveo/devcontainer-features/local-pkgsite:1": {
"port": "6060"
},
"ghcr.io/thediveo/devcontainer-features/pinact:0": {},
"ghcr.io/thediveo/devcontainer-features/go-mod-upgrade:1": {},
"ghcr.io/thediveo/devcontainer-features/lazygit:0": {},
"ghcr.io/devcontainers/features/docker-in-docker:4": {
"version": "latest",
"moby": false // go for the upstream Docker-CE
},
"ghcr.io/thediveo/devcontainer-features/lazygit:0": {},
"ghcr.io/thediveo/devcontainer-features/local-pkgsite:0": {},
"ghcr.io/thediveo/devcontainer-features/goreportcard:0": {},
"ghcr.io/thediveo/devcontainer-features/go-mod-upgrade:0": {},
"ghcr.io/thediveo/devcontainer-features/gocover:0": {
"root": true,
"num-programs": "1",
"race": true,
"ghcr.io/thediveo/devcontainer-features/gocover:1": {
"root": true, // test both as ordinary user and root
"num-programs": "1", // do never run package-level tests in parallel
"race": true, // enable race detector
"verbose": true,
"html": true
},
"ghcr.io/thediveo/devcontainer-features/pin-github-action:0": {}
"html": true // generate coverage.html
}
},
"securityOpt": [
"apparmor=unconfined"
],
"remoteEnv": {
"GOPATH": "/home/vscode/go",
"PATH": "/home/vscode/go/bin:/go/bin:/usr/local/go/bin:${localEnv:PATH}"
},
"customizations": {
"vscode": {
"extensions": [
"stkb.rewrap",
"dnut.rewrap-revived",
"brunnerh.insert-unicode",
"mhutchie.git-graph",
"ms-vscode.makefile-tools"
"ms-vscode.makefile-tools",
"docker.docker"
]
}
}
Expand Down
12 changes: 10 additions & 2 deletions apps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path"
"time"

"github.com/jmoiron/sqlx"
"github.com/thediveo/lxkns/model"

. "github.com/onsi/ginkgo/v2"
Expand All @@ -26,12 +27,19 @@ var _ = Describe("IED app engine installed apps", func() {
})
})

It("reads installed app information", func() {
It("self-tests db", func() {
db := Successful(sqlx.Open(dbDriverName,
path.Join(Successful(os.Getwd()), "tests/sqlite-alpine-appengine-db/test-apps-and-device.db?mode=ro")))
defer func() { Expect(db.Close()).To(Succeed()) }()
Expect(db.Ping()).To(Succeed())
})

FIt("reads installed app information", func() {
// Use a local test database, so we don't need to rely on an (fake) edge
// core running.
cwd := Successful(os.Getwd())
db := Successful(open(path.Join(cwd, "tests/sqlite-alpine-appengine-db/test-apps-and-device.db"), model.PIDType(os.Getpid())))
defer func() { _ = db.Close() }()
defer func() { Expect(db.Close()).To(Succeed()) }()

apps := Successful(db.Apps())
Expect(apps).To(HaveLen(4))
Expand Down
58 changes: 15 additions & 43 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package ieddata

import (
"fmt"
"io"
"os"
"path"
"regexp"
Expand All @@ -15,7 +14,9 @@ import (
"github.com/jmoiron/sqlx"
"github.com/thediveo/lxkns/model"
"github.com/thediveo/procfsroot"
_ "modernc.org/sqlite"

"modernc.org/sqlite"
"modernc.org/sqlite/vfs"
)

// PlatformBoxDb is the file name of the platform box database.
Expand Down Expand Up @@ -78,63 +79,34 @@ func sanitize(basename string) string {
// it in some tests without the need for a correctly set-up fake edge runtime
// container. Please note that any caller must have sanitized the name parameter
// first.
//
// As it turns out there are some situations which we don't yet fully understand
// but that causes opening the database via a proc path to fail, even if it
// succeeds in other situations. Interestingly, the termdbms sqlite3 TUI works
// in all these situation and an analysis of its source code base reveals that
// it simply makes a copy of the original database, see
// https://github.com/mathaou/termdbms/blob/be6f397196077cc7c9ced86e6460470e3b223f3e/main.go#L132.
//
// Well, what's good for the goose is good for the gander, so copy it is. Sigh.
func open(name string, pid model.PIDType) (*AppEngineDB, error) {
rootpath := fmt.Sprintf("/proc/%d/root", pid)
dbpath, err := procfsroot.EvalSymlinks(name, rootpath, procfsroot.EvalFullPath)
if err != nil {
return nil, fmt.Errorf("cannot determine full database path, reason: %w", err)
}
dbpath = path.Join(rootpath, dbpath)

// Make a temporary copy of the database so we can open it successfully in
// all our cases.
origdbf, err := os.Open(dbpath)
root, err := os.OpenRoot(rootpath)
if err != nil {
return nil, fmt.Errorf("unable to open database, reason: %w", err)
return nil, fmt.Errorf("cannot open root, reason: %w", err)
}
defer func() { _ = origdbf.Close() }()
tmpdbf, err := os.CreateTemp("", "temp-db-copy-*")
defer func() { _ = root.Close() }()
vfsid, sqlvfs, err := vfs.New(root.FS())
if err != nil {
return nil, fmt.Errorf("unable to open database, reason: %w", err)
}
defer func() { _ = tmpdbf.Close() }()
if _, err := io.Copy(tmpdbf, origdbf); err != nil {
_ = os.Remove(tmpdbf.Name())
return nil, fmt.Errorf("unable to open database, reason: %w", err)
return nil, fmt.Errorf("cannot create sqlite VFS, reason: %w", err)
}
defer func() { _ = sqlvfs.Close() }()

// When available, make a copy of the accompanying WAL file also.
if walf, err := os.Open(dbpath + "-wal"); err == nil {
defer func() { _ = walf.Close() }()
if tmpwalf, err := os.Create(tmpdbf.Name() + "-wal"); err == nil {
defer func() { _ = tmpwalf.Close() }()
if _, err := io.Copy(tmpwalf, walf); err != nil {
_ = os.Remove(tmpwalf.Name())
}
_ = tmpwalf.Close()
}
_ = walf.Close()
}

// As sql.Open might just "validate its parameters" and this might mean near
// to nothing, we explicitly ping the database in order to see that it is
// okay.
dbpath = tmpdbf.Name()
_ = tmpdbf.Close()
db, err := sqlx.Open(dbDriverName, dbpath)
dbpath = "./" + path.Join("." /* sic! */, dbpath)
db, err := sqlx.Open(dbDriverName, dbpath+"?vfs="+vfsid)
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
if sqlerr, ok := err.(*sqlite.Error); ok {
return nil, fmt.Errorf("ping database (%s)%q: sqlite error: %s",
rootpath, dbpath, sqlite.ErrorCodeString[sqlerr.Code()])
}
return nil, err
}

Expand Down
50 changes: 49 additions & 1 deletion db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ package ieddata

import (
"context"
"database/sql"
"fmt"
"os"
"path"
"strconv"
"time"

"github.com/jmoiron/sqlx"
"golang.org/x/sys/unix"
"modernc.org/sqlite/vfs"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -87,7 +90,7 @@ var _ = Describe("IED app engine database", func() {

It("fails for missing/invalid IED app engine database", func() {
Expect(Open("foo.db")).Error().To(MatchError(ContainSubstring("/root/data")))
Expect(Open("not.a.db")).Error().To(MatchError(ContainSubstring("unable to open database")))
Expect(Open("not.a.db")).Error().To(MatchError(ContainSubstring("nable to open database")))
})

It("accesses the app engine database", func() {
Expand All @@ -106,4 +109,49 @@ var _ = Describe("IED app engine database", func() {
Expect(m).To(HaveKeyWithValue("ownerEmail", "foo.bar@example.com"))
})

Context("doing weird things with os.Root on /proc/PID/root", func() {

It("reads a db via vfs on /proc/PID/root", func() {
By("creating a temporary directory for a test database")
tmpdbdir := Successful(os.MkdirTemp("", "canarydb-*"))
defer func() { _ = os.RemoveAll(tmpdbdir) }()

const dbname = "canary.db"

By("creating the testing database")
func() {
db := Successful(sql.Open(dbDriverName,
"file:"+path.Join(tmpdbdir, dbname)))
defer func() { Expect(db.Close()).To(Succeed()) }()

Expect(db.Exec("create table 'test' ('name' varchar(32) not null, primary key('name') )")).
Error().NotTo(HaveOccurred())
Expect(db.Exec("insert into 'test' (name) values ('foobar')")).
Error().NotTo(HaveOccurred())
}()

By("opening the db via VFS")
root := Successful(os.OpenRoot(
path.Join("/proc", strconv.FormatInt(int64(os.Getpid()), 10), "root", tmpdbdir)))
defer func() { Expect(root.Close()).To(Succeed()) }()

vfsid, sqlvfs := Successful2R(vfs.New(root.FS()))
defer func() { Expect(sqlvfs.Close()).To(Succeed()) }()

db := Successful(sqlx.Open(dbDriverName, dbname+"?vfs="+vfsid))
defer func() { Expect(db.Close()).To(Succeed()) }()

Expect(db.Ping()).To(Succeed())

By("reading a row from the read-only db")
rows := Successful(db.Query("select * from 'test'"))
defer func() { Expect(rows.Close()).To(Succeed()) }()
Expect(rows.Next()).To(BeTrue())
var name string
Expect(rows.Scan(&name)).To(Succeed())
Expect(name).To(Equal("foobar"))
})

})

})
Loading
Loading