diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 952a2af..b8645dd 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.21 + go-version: 1.22 - name: lint run: | @@ -30,5 +30,5 @@ jobs: - name: Build run: go build -v ./... - - name: Test - run: go test -v ./... + - name: CI + run: make ci diff --git a/Dockerfile.dapper b/Dockerfile.dapper index 09b1799..f09f80c 100644 --- a/Dockerfile.dapper +++ b/Dockerfile.dapper @@ -1,4 +1,4 @@ -FROM registry.suse.com/bci/golang:1.21 +FROM registry.suse.com/bci/golang:1.22 ARG DAPPER_HOST_ARCH ENV HOST_ARCH=${DAPPER_HOST_ARCH} ARCH=${DAPPER_HOST_ARCH} diff --git a/command/command.go b/command/command.go new file mode 100644 index 0000000..246cae9 --- /dev/null +++ b/command/command.go @@ -0,0 +1,90 @@ +package command + +import ( + "bytes" + "os/exec" + "path/filepath" + "time" + + "github.com/pkg/errors" +) + +var ErrCmdTimeout = errors.New("command timeout") + +const ( + NSBinary = "nsenter" + cmdTimeoutDefault = 180 * time.Second // 3 minutes by default + cmdTimeoutNone = 0 * time.Second // no timeout +) + +type Executor struct { + namespace string + cmdTimeout time.Duration +} + +func NewExecutor() *Executor { + return &Executor{ + namespace: "", + cmdTimeout: cmdTimeoutDefault, + } +} + +func NewExecutorWithNS(ns string) (*Executor, error) { + exec := NewExecutor() + exec.namespace = ns + + // test if nsenter is available + if _, err := execute(NSBinary, []string{"-V"}, cmdTimeoutNone); err != nil { + return nil, errors.Wrap(err, "cannot find nsenter for namespace switching") + } + return exec, nil +} + +func (exec *Executor) SetTimeout(timeout time.Duration) { + exec.cmdTimeout = timeout +} + +func (exec *Executor) Execute(cmd string, args []string) (string, error) { + command := cmd + cmdArgs := args + if exec.namespace != "" { + cmdArgs = []string{ + "--mount=" + filepath.Join(exec.namespace, "mnt"), + "--net=" + filepath.Join(exec.namespace, "net"), + "--ipc=" + filepath.Join(exec.namespace, "ipc"), + cmd, + } + command = NSBinary + cmdArgs = append(cmdArgs, args...) + } + return execute(command, cmdArgs, exec.cmdTimeout) +} + +func execute(command string, args []string, timeout time.Duration) (string, error) { + cmd := exec.Command(command, args...) + + var output, stderr bytes.Buffer + cmdTimeout := false + cmd.Stdout = &output + cmd.Stderr = &stderr + + timer := time.NewTimer(cmdTimeoutNone) + if timeout != cmdTimeoutNone { + // add timer to kill the process if timeout + timer = time.AfterFunc(timeout, func() { + cmdTimeout = true + cmd.Process.Kill() + }) + } + defer timer.Stop() + + if err := cmd.Run(); err != nil { + if cmdTimeout { + return "", errors.Wrapf(ErrCmdTimeout, "timeout after %v: %v %v", timeout, command, args) + } + return "", errors.Wrapf(err, "failed to execute: %v %v, output %s, stderr %s", + command, args, output.String(), stderr.String()) + } + + return output.String(), nil +} diff --git a/command/command_test.go b/command/command_test.go new file mode 100644 index 0000000..c7656a3 --- /dev/null +++ b/command/command_test.go @@ -0,0 +1,46 @@ +package command + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type CommandTestSuite struct { + suite.Suite + executor *Executor +} + +func (suite *CommandTestSuite) SetupTest() { + suite.executor = NewExecutor() +} + +func (suite *CommandTestSuite) TestNewExecutor() { + assert.NotNil(suite.T(), suite.executor) +} + +func (suite *CommandTestSuite) TestSetTimeout() { + suite.executor.SetTimeout(5 * time.Second) + assert.Equal(suite.T(), 5*time.Second, suite.executor.cmdTimeout) +} + +func (suite *CommandTestSuite) TestExecute() { + output, err := suite.executor.Execute("echo", []string{"hello", "world"}) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "hello world\n", output) +} + +func (suite *CommandTestSuite) TestExecute_Timeout() { + suite.executor.SetTimeout(1 * time.Second) + assert.Equal(suite.T(), 1*time.Second, suite.executor.cmdTimeout) + output, err := suite.executor.Execute("sleep", []string{"5"}) + assert.NotNil(suite.T(), err) + assert.Contains(suite.T(), err.Error(), "command timeout") + assert.Empty(suite.T(), output) +} + +func TestCommandTestSuite(t *testing.T) { + suite.Run(t, new(CommandTestSuite)) +} diff --git a/common/ns.go b/common/ns.go new file mode 100644 index 0000000..65d14e7 --- /dev/null +++ b/common/ns.go @@ -0,0 +1,73 @@ +package common + +import ( + "fmt" + + "github.com/prometheus/procfs" +) + +const ( + DockerdProcess = "dockerd" + ContainerdProcess = "containerd" + ContainerdProcessShim = "containerd-shim" +) + +func getPidProc(hostProcPath string, pid int) (*procfs.Proc, error) { + fs, err := procfs.NewFS(hostProcPath) + if err != nil { + return nil, err + } + proc, err := fs.Proc(pid) + if err != nil { + return nil, err + } + return &proc, nil +} + +func getSelfProc(hostProcPath string) (*procfs.Proc, error) { + fs, err := procfs.NewFS(hostProcPath) + if err != nil { + return nil, err + } + proc, err := fs.Self() + if err != nil { + return nil, err + } + return &proc, nil +} + +func findAncestorByName(hostProcPath string, ancestorProcess string) (*procfs.Proc, error) { + proc, err := getSelfProc(hostProcPath) + if err != nil { + return nil, err + } + + for { + st, err := proc.Stat() + if err != nil { + return nil, err + } + if st.Comm == ancestorProcess { + return proc, nil + } + if st.PPID == 0 { + break + } + proc, err = getPidProc(hostProcPath, st.PPID) + if err != nil { + return nil, err + } + } + return nil, fmt.Errorf("failed to find the ancestor process: %s", ancestorProcess) +} + +func GetHostNamespacePath(hostProcPath string) string { + containerNames := []string{DockerdProcess, ContainerdProcess, ContainerdProcessShim} + for _, name := range containerNames { + proc, err := findAncestorByName(hostProcPath, name) + if err == nil { + return fmt.Sprintf("%s/%d/ns/", hostProcPath, proc.PID) + } + } + return fmt.Sprintf("%s/%d/ns/", hostProcPath, 1) +} diff --git a/common/ns_test.go b/common/ns_test.go new file mode 100644 index 0000000..1789d23 --- /dev/null +++ b/common/ns_test.go @@ -0,0 +1,52 @@ +package common + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type NSTestSuite struct { + suite.Suite + hostProcPath string +} + +func (suite *NSTestSuite) SetupTest() { + suite.hostProcPath = "/proc" +} + +func TestNSTestSuite(t *testing.T) { + suite.Run(t, new(NSTestSuite)) +} + +func (suite *NSTestSuite) TestGetPidProc() { + pid := 66666 // should not exists + proc, err := getPidProc(suite.hostProcPath, pid) + + assert.NotNil(suite.T(), err) + assert.Nil(suite.T(), proc) + + pid = 1 + proc, err = getPidProc(suite.hostProcPath, pid) + + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), proc) + assert.Equal(suite.T(), pid, proc.PID) +} + +func (suite *NSTestSuite) TestGetSelfProc() { + proc, err := getSelfProc(suite.hostProcPath) + + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), proc) + assert.Equal(suite.T(), os.Getpid(), proc.PID) +} + +func (suite *NSTestSuite) TestGetHostNamespacePath() { + expectedPath := "/proc/1/ns/" + path := GetHostNamespacePath(suite.hostProcPath) + + assert.Equal(suite.T(), expectedPath, path) +} diff --git a/random.go b/common/random.go similarity index 96% rename from random.go rename to common/random.go index 3c4003c..82e0ad7 100644 --- a/random.go +++ b/common/random.go @@ -1,4 +1,4 @@ -package gocommon +package common import ( "crypto/rand" diff --git a/random_test.go b/common/random_test.go similarity index 98% rename from random_test.go rename to common/random_test.go index 05b1547..0212d7c 100644 --- a/random_test.go +++ b/common/random_test.go @@ -1,4 +1,4 @@ -package gocommon +package common import ( "testing" diff --git a/map.go b/ds/map.go similarity index 98% rename from map.go rename to ds/map.go index 036dac6..05c40f3 100644 --- a/map.go +++ b/ds/map.go @@ -1,4 +1,4 @@ -package gocommon +package ds // MapFilterFunc iterates over elements of map, returning a map of all elements // the function f returns truthy for. The function f is invoked with three diff --git a/map_test.go b/ds/map_test.go similarity index 98% rename from map_test.go rename to ds/map_test.go index 478ffed..62dc84f 100644 --- a/map_test.go +++ b/ds/map_test.go @@ -1,4 +1,4 @@ -package gocommon +package ds import ( "testing" diff --git a/slice.go b/ds/slice.go similarity index 99% rename from slice.go rename to ds/slice.go index 9efebda..63e0391 100644 --- a/slice.go +++ b/ds/slice.go @@ -1,4 +1,4 @@ -package gocommon +package ds import "slices" diff --git a/slice_test.go b/ds/slice_test.go similarity index 99% rename from slice_test.go rename to ds/slice_test.go index 8f6976f..79a14d8 100644 --- a/slice_test.go +++ b/ds/slice_test.go @@ -1,4 +1,4 @@ -package gocommon +package ds import ( "strings" diff --git a/files.go b/files/files.go similarity index 99% rename from files.go rename to files/files.go index 945a304..160cd17 100644 --- a/files.go +++ b/files/files.go @@ -1,4 +1,4 @@ -package gocommon +package files import ( "fmt" diff --git a/go.mod b/go.mod index cc62859..451f633 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/harvester/go-common -go 1.21 +go 1.22 require ( github.com/coreos/go-systemd/v22 v22.5.0 @@ -11,9 +11,13 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) +require github.com/prometheus/procfs v0.15.1 + +require github.com/pkg/errors v0.9.1 + require ( github.com/Masterminds/semver/v3 v3.2.1 github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect + golang.org/x/sys v0.20.0 // indirect ) diff --git a/go.sum b/go.sum index d4d6cda..6efa16a 100644 --- a/go.sum +++ b/go.sum @@ -9,16 +9,23 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gocommon.go b/gocommon.go deleted file mode 100644 index 256021b..0000000 --- a/gocommon.go +++ /dev/null @@ -1,22 +0,0 @@ -/* - * This repo was part of the harvester project, copied to this project - * to get around private package issues. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Copyright 2023 SUSE, LLC. - * - */ - -// Dummy package -package gocommon diff --git a/scripts/ci b/scripts/ci new file mode 100755 index 0000000..d8edeff --- /dev/null +++ b/scripts/ci @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +cd $(dirname $0) + +./test +./validate \ No newline at end of file diff --git a/systemd-dbus.go b/sys/systemd-dbus.go similarity index 96% rename from systemd-dbus.go rename to sys/systemd-dbus.go index 72f9779..0e00c68 100644 --- a/systemd-dbus.go +++ b/sys/systemd-dbus.go @@ -1,4 +1,4 @@ -package gocommon +package sys import ( "context" diff --git a/watcher.go b/sys/watcher.go similarity index 99% rename from watcher.go rename to sys/watcher.go index 104bcaf..8927e94 100644 --- a/watcher.go +++ b/sys/watcher.go @@ -1,4 +1,4 @@ -package gocommon +package sys import ( "context" diff --git a/watcher_test.go b/sys/watcher_test.go similarity index 98% rename from watcher_test.go rename to sys/watcher_test.go index 0c9b712..1d3c68f 100644 --- a/watcher_test.go +++ b/sys/watcher_test.go @@ -1,4 +1,4 @@ -package gocommon +package sys import ( "reflect"