diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 995b277..b3acb8c 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -2,6 +2,7 @@
+
\ No newline at end of file
diff --git a/cmd/bifroest/common.go b/cmd/bifroest/common.go
new file mode 100644
index 0000000..8906b48
--- /dev/null
+++ b/cmd/bifroest/common.go
@@ -0,0 +1,11 @@
+package main
+
+import goos "os"
+
+func workingDirectory() string {
+ v, err := goos.Getwd()
+ if err == nil {
+ return v
+ }
+ return "/"
+}
diff --git a/cmd/bifroest/exec.go b/cmd/bifroest/exec.go
new file mode 100644
index 0000000..7e3eb52
--- /dev/null
+++ b/cmd/bifroest/exec.go
@@ -0,0 +1,101 @@
+package main
+
+import (
+ goos "os"
+ "os/exec"
+ "os/signal"
+ "syscall"
+
+ "github.com/alecthomas/kingpin/v2"
+
+ "github.com/engity-com/bifroest/pkg/errors"
+ "github.com/engity-com/bifroest/pkg/sys"
+)
+
+var _ = registerCommand(func(app *kingpin.Application) {
+ opts := execOpts{
+ workingDirectory: workingDirectory(),
+ environment: sys.EnvVars{},
+ }
+
+ cmd := app.Command("exec", "Runs a given process with the given attributes (environment, working directory, ...).").
+ Hidden().
+ Action(func(*kingpin.ParseContext) error {
+ return doExec(&opts)
+ })
+ cmd.Flag("workingDir", "Directory to start in.").
+ Short('d').
+ Default(opts.workingDirectory).
+ PlaceHolder("").
+ StringVar(&opts.workingDirectory)
+ cmd.Flag("executable", "Path to executable to be used. If not defined, first argument will be used.").
+ Short('p').
+ Default(opts.path).
+ PlaceHolder("").
+ StringVar(&opts.path)
+ cmd.Flag("env", "Environment variables to execute the process with.").
+ Short('e').
+ StringMapVar(&opts.environment)
+ cmd.Arg("command", "Command to execute.").
+ Required().
+ StringsVar(&opts.argv)
+
+ registerExecCmdFlags(cmd, &opts)
+})
+
+func doExec(opts *execOpts) error {
+ fail := func(err error) error {
+ return err
+ }
+ failf := func(msg string, args ...any) error {
+ return fail(errors.System.Newf(msg, args...))
+ }
+
+ cmd := exec.Cmd{
+ Dir: opts.workingDirectory,
+ SysProcAttr: &syscall.SysProcAttr{},
+ Env: (sys.EnvVars(opts.environment)).Strings(),
+ Stderr: goos.Stderr,
+ Stdin: goos.Stdin,
+ Stdout: goos.Stdout,
+ Args: opts.argv,
+ Path: opts.path,
+ }
+
+ if cmd.Path == "" && len(cmd.Args) > 0 {
+ cmd.Path = cmd.Args[0]
+ }
+ var err error
+ if cmd.Path, err = exec.LookPath(cmd.Path); err != nil {
+ return fail(err)
+ }
+
+ if err := enrichExecCmd(&cmd, opts); err != nil {
+ return failf("cannot apply execution parameters to %v: :%w", cmd.Args, err)
+ }
+
+ sigs := make(chan goos.Signal, 1)
+ defer close(sigs)
+ signal.Notify(sigs)
+
+ go func() {
+ for {
+ sig, ok := <-sigs
+ if !ok {
+ return
+ }
+ _ = cmd.Process.Signal(sig)
+ }
+ }()
+
+ err = cmd.Run()
+ var eErr *exec.ExitError
+ if errors.As(err, &eErr) {
+ goos.Exit(eErr.ExitCode())
+ return nil
+ } else if err != nil {
+ return fail(err)
+ } else {
+ return nil
+ }
+}
diff --git a/cmd/bifroest/exec_unix.go b/cmd/bifroest/exec_unix.go
new file mode 100644
index 0000000..38a7c7d
--- /dev/null
+++ b/cmd/bifroest/exec_unix.go
@@ -0,0 +1,71 @@
+//go:build unix
+
+package main
+
+import (
+ "os/exec"
+ "os/user"
+ "strconv"
+ "syscall"
+
+ "github.com/alecthomas/kingpin/v2"
+
+ "github.com/engity-com/bifroest/pkg/errors"
+)
+
+type execOpts struct {
+ workingDirectory string
+ environment map[string]string
+ user, group string
+ path string
+ argv []string
+}
+
+func registerExecCmdFlags(cmd *kingpin.CmdClause, opts *execOpts) {
+ cmd.Flag("user", "User the process should run with.").
+ Default(opts.user).
+ Short('u').
+ StringVar(&opts.user)
+ cmd.Flag("group", "Group the process should run with.").
+ Default(opts.group).
+ Short('g').
+ StringVar(&opts.group)
+}
+
+func enrichExecCmd(cmd *exec.Cmd, with *execOpts) error {
+ if plainUser := with.user; plainUser != "" {
+ cmd.SysProcAttr.Credential = &syscall.Credential{}
+
+ u, err := user.LookupId(plainUser)
+ var uuiErr *user.UnknownUserIdError
+ if errors.As(err, &uuiErr) {
+ u, err = user.Lookup(plainUser)
+ }
+ if err != nil {
+ return err
+ }
+ if v, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
+ return err
+ } else {
+ cmd.SysProcAttr.Credential.Uid = uint32(v)
+ }
+
+ if plainGroup := with.group; plainGroup != "" {
+ g, err := user.LookupGroupId(plainGroup)
+ var ugiErr *user.UnknownGroupIdError
+ if errors.As(err, &ugiErr) {
+ g, err = user.LookupGroup(plainGroup)
+ }
+ if err != nil {
+ return err
+ }
+ if v, err := strconv.ParseUint(g.Gid, 10, 32); err != nil {
+ return err
+ } else {
+ cmd.SysProcAttr.Credential.Gid = uint32(v)
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/cmd/bifroest/exec_windows.go b/cmd/bifroest/exec_windows.go
new file mode 100644
index 0000000..a0e826f
--- /dev/null
+++ b/cmd/bifroest/exec_windows.go
@@ -0,0 +1,23 @@
+//go:build windows
+
+package main
+
+import (
+ "os/exec"
+
+ "github.com/alecthomas/kingpin/v2"
+)
+
+type execOpts struct {
+ workingDirectory string
+ environment map[string]string
+ path string
+ argv []string
+}
+
+func registerExecCmdFlags(_ *kingpin.CmdClause, _ *execOpts) {
+}
+
+func enrichExecCmd(_ *exec.Cmd, _ *execOpts) error {
+ return nil
+}
diff --git a/cmd/bifroest/imp-init.go b/cmd/bifroest/imp-init.go
new file mode 100644
index 0000000..4ba9a1e
--- /dev/null
+++ b/cmd/bifroest/imp-init.go
@@ -0,0 +1,60 @@
+package main
+
+import (
+ "io"
+ goos "os"
+ "path/filepath"
+
+ "github.com/alecthomas/kingpin/v2"
+ log "github.com/echocat/slf4g"
+
+ "github.com/engity-com/bifroest/pkg/common"
+ "github.com/engity-com/bifroest/pkg/errors"
+ "github.com/engity-com/bifroest/pkg/imp"
+ "github.com/engity-com/bifroest/pkg/sys"
+)
+
+var _ = registerCommand(func(app *kingpin.Application) {
+ targetPath := imp.DefaultInitPath
+
+ cmd := app.Command("imp-init", "Prepares the environment to run Bifröst's imp inside.").
+ Hidden().
+ Action(func(*kingpin.ParseContext) error {
+ return doImpInit(targetPath)
+ })
+ cmd.Flag("targetPath", "Path to prepare.").
+ Default(targetPath).
+ PlaceHolder("").
+ StringVar(&targetPath)
+})
+
+func doImpInit(targetPath string) (rErr error) {
+ log.WithAll(sys.VersionToMap(versionV)).
+ With("targetPath", targetPath).
+ Info("initialize target path for Engity's Bifröst imp...")
+
+ self, err := goos.Executable()
+ if err != nil {
+ return errors.System.Newf("cannot detect own location: %w", err)
+ }
+
+ sf, err := goos.Open(self)
+ if err != nil {
+ return errors.System.Newf("cannot open self (%s) for reading: %w", self, err)
+ }
+ defer common.IgnoreCloseError(sf)
+
+ _ = goos.MkdirAll(targetPath, 0755)
+ targetFile := filepath.Join(targetPath, versionV.Os().AppendExtToFilename("bifroest"))
+ tf, err := goos.OpenFile(targetFile, goos.O_CREATE|goos.O_TRUNC|goos.O_WRONLY, 0755)
+ if err != nil {
+ return errors.System.Newf("cannot open target file (%s) for writing: %w", targetFile, err)
+ }
+ defer common.KeepCloseError(&rErr, tf)
+
+ if _, err := io.Copy(tf, sf); err != nil {
+ return errors.System.Newf("cannot copy %s to %s: %w", self, targetFile, err)
+ }
+
+ return nil
+}
diff --git a/cmd/bifroest/run.go b/cmd/bifroest/run.go
index d4ef928..94df372 100644
--- a/cmd/bifroest/run.go
+++ b/cmd/bifroest/run.go
@@ -17,7 +17,7 @@ var _ = registerCommand(func(app *kingpin.Application) {
configureRunCmd(app)
})
-func doRunDefault(conf configuration.ConfigurationRef) error {
+func doRunDefault(conf configuration.Ref) error {
svc := service.Service{
Configuration: *conf.Get(),
Version: versionV,
diff --git a/cmd/bifroest/run_unix.go b/cmd/bifroest/run_unix.go
index 764eac6..c6ae1a1 100644
--- a/cmd/bifroest/run_unix.go
+++ b/cmd/bifroest/run_unix.go
@@ -13,7 +13,7 @@ const (
)
func configureRunCmd(app *kingpin.Application) *kingpin.Application {
- var conf configuration.ConfigurationRef
+ var conf configuration.Ref
cmd := app.Command("run", "Runs the service.").
Action(func(*kingpin.ParseContext) error {
return doRun(conf)
@@ -26,6 +26,6 @@ func configureRunCmd(app *kingpin.Application) *kingpin.Application {
return app
}
-func doRun(conf configuration.ConfigurationRef) error {
+func doRun(conf configuration.Ref) error {
return doRunDefault(conf)
}
diff --git a/cmd/bifroest/run_windows.go b/cmd/bifroest/run_windows.go
index 5274ef1..26614fa 100644
--- a/cmd/bifroest/run_windows.go
+++ b/cmd/bifroest/run_windows.go
@@ -21,7 +21,7 @@ const (
func configureRunCmd(app *kingpin.Application) *kingpin.Application {
var ws windowsService
- var conf configuration.ConfigurationRef
+ var conf configuration.Ref
cmd := app.Command("run", "Runs the service.").
Action(func(*kingpin.ParseContext) error {
return doRun(conf, &ws)
@@ -36,7 +36,7 @@ func configureRunCmd(app *kingpin.Application) *kingpin.Application {
}
-func doRun(conf configuration.ConfigurationRef, ws *windowsService) error {
+func doRun(conf configuration.Ref, ws *windowsService) error {
inService, err := svc.IsWindowsService()
if err != nil {
return errors.System.Newf("failed to determine if we are running in service: %w", err)
diff --git a/cmd/bifroest/service_windows.go b/cmd/bifroest/service_windows.go
index 7cbd87b..3a54613 100644
--- a/cmd/bifroest/service_windows.go
+++ b/cmd/bifroest/service_windows.go
@@ -27,7 +27,7 @@ const (
type windowsService struct {
name string
- conf configuration.ConfigurationRef
+ conf configuration.Ref
logger *eventlog.Log
}
@@ -42,7 +42,7 @@ func (this *windowsService) registerFlagsAt(cmd *kingpin.CmdClause) *kingpin.Cmd
var _ = registerCommand(func(app *kingpin.Application) {
svcCmd := app.Command("service", "")
- var conf configuration.ConfigurationRef
+ var conf configuration.Ref
var svc windowsService
common := func(cmd *kingpin.CmdClause) *kingpin.CmdClause {
return svc.registerFlagsAt(cmd)
@@ -123,7 +123,7 @@ func (this *windowsService) Execute(_ []string, r <-chan svc.ChangeRequest, chan
return false, 0
}
-func (this *windowsService) install(conf configuration.ConfigurationRef, start, retry bool) error {
+func (this *windowsService) install(conf configuration.Ref, start, retry bool) error {
if err := conf.MakeAbsolute(); err != nil {
return err
}
diff --git a/cmd/bifroest/sftp-server.go b/cmd/bifroest/sftp-server.go
index aeba05a..5111c0e 100644
--- a/cmd/bifroest/sftp-server.go
+++ b/cmd/bifroest/sftp-server.go
@@ -8,30 +8,23 @@ import (
"github.com/engity-com/bifroest/pkg/sftp"
)
-var (
- workingDir = func() string {
- v, err := goos.Getwd()
- if err == nil {
- return v
- }
- return "/"
- }()
-)
-
var _ = registerCommand(func(app *kingpin.Application) {
+ cwd := workingDirectory()
+
cmd := app.Command("sftp-server", "Run the sftp server instance used by this instance.").
Hidden().
Action(func(*kingpin.ParseContext) error {
- return doSftpServer()
+ return doSftpServer(cwd)
})
- cmd.Flag("workingDir", "Directory to start in. Default: "+workingDir).
+ cmd.Flag("workingDir", "Directory to start in.").
+ Default(cwd).
PlaceHolder("").
- StringVar(&workingDir)
+ StringVar(&cwd)
})
-func doSftpServer() error {
+func doSftpServer(cwd string) error {
s := sftp.Server{
- WorkingDir: workingDir,
+ WorkingDir: cwd,
}
if err := s.Run(&stdpipe{}); err != nil {
diff --git a/cmd/build/build-artifact.go b/cmd/build/build-artifact.go
index e9e9be9..1c8a3bc 100644
--- a/cmd/build/build-artifact.go
+++ b/cmd/build/build-artifact.go
@@ -65,6 +65,7 @@ func (this *buildArtifact) Close() (rErr error) {
defer this.lock.Unlock()
for _, closer := range this.onClose {
+ //goland:noinspection GoDeferInLoop
defer common.KeepError(&rErr, closer)
}
@@ -119,6 +120,7 @@ type buildArtifacts []*buildArtifact
func (this buildArtifacts) Close() (rErr error) {
for _, v := range this {
+ //goland:noinspection GoDeferInLoop
defer common.KeepCloseError(&rErr, v)
}
return nil
diff --git a/cmd/build/build-image.go b/cmd/build/build-image.go
index 58c0d9d..ed47edb 100644
--- a/cmd/build/build-image.go
+++ b/cmd/build/build-image.go
@@ -230,6 +230,7 @@ func (this *buildImage) merge(ctx context.Context, as buildArtifacts) (_ buildAr
if err != nil {
return nil, err
}
+ //goland:noinspection GoDeferInLoop
defer common.IgnoreCloseErrorIfFalse(&success, a)
if a != nil {
result = append(result, a)
diff --git a/cmd/build/dependencies-images-files.go b/cmd/build/dependencies-images-files.go
index 72d7c07..07c3157 100644
--- a/cmd/build/dependencies-images-files.go
+++ b/cmd/build/dependencies-images-files.go
@@ -195,6 +195,7 @@ func (this *dependenciesImagesFiles) getFileFromLayer(layer v1.Layer, sourceFn,
if err != nil {
return failf("cannot create file %q: %w", targetFn, err)
}
+ //goland:noinspection GoDeferInLoop
defer common.KeepCloseError(&rErr, to)
if _, err := io.Copy(to, tr); err != nil {
diff --git a/contrib/systemd/bifroest-in-docker.service b/contrib/systemd/bifroest-in-docker.service
index e14350a..a8db792 100644
--- a/contrib/systemd/bifroest-in-docker.service
+++ b/contrib/systemd/bifroest-in-docker.service
@@ -9,7 +9,7 @@ After=docker.service
Environment=IMAGE=ghcr.io/engity-com/bifroest:latest
# Comment this line out if you don't want always the latest version of Bifröst
ExecStartPre=/usr/bin/docker pull ${IMAGE}
-ExecStartPre=mkdir -p /var/lib/engity/bifroest
+ExecStartPre=/usr/bin/mkdir -p /var/lib/engity/bifroest
ExecStart=/usr/bin/docker run --rm --name bifroest -p 22:22 -v /var/run/docker.sock:/var/run/docker.sock -v /etc/engity/bifroest:/etc/engity/bifroest -v /var/lib/engity/bifroest:/var/lib/engity/bifroest ${IMAGE} run --log.colorMode=always
Restart=always
Type=simple
diff --git a/docs/reference/authorization/htpasswd.md b/docs/reference/authorization/htpasswd.md
index c93cc5e..3a610cb 100644
--- a/docs/reference/authorization/htpasswd.md
+++ b/docs/reference/authorization/htpasswd.md
@@ -5,7 +5,7 @@ description: How to authorize an user request via credentials stored in htpasswd
# Htpasswd authorization
-Authorizes an user request via credentials stored in [htpasswd format](#format).
+Authorizes a user request via credentials stored in [htpasswd format](#format).
## Properties
diff --git a/docs/reference/authorization/oidc.md b/docs/reference/authorization/oidc.md
index 110e2bc..bbc937b 100644
--- a/docs/reference/authorization/oidc.md
+++ b/docs/reference/authorization/oidc.md
@@ -10,7 +10,7 @@ There is no need that the actual user exists in any way on the host machine of B
This provides an easy way for SSO in all types of organizations, small or big. See [use cases for more details](../../usecases.md).
-Currently the following flow of OpenID Connect is supported:
+Currently, the following flow of OpenID Connect is supported:
* [Device Auth](#device-auth)
diff --git a/docs/reference/data-type.md b/docs/reference/data-type.md
index cbefeb3..5683e1d 100644
--- a/docs/reference/data-type.md
+++ b/docs/reference/data-type.md
@@ -114,6 +114,7 @@ Can be one of:
Can be one of:
* `ifAbsend`
* `always`
+* `never`
## Regex
Regular expression of [Go flavor](https://pkg.go.dev/regexp). You can play around with it at [regex.com](https://regex101.com/r/fRdVOl/1).
diff --git a/docs/reference/environment/docker.md b/docs/reference/environment/docker.md
index 2ff9f43..4d60810 100644
--- a/docs/reference/environment/docker.md
+++ b/docs/reference/environment/docker.md
@@ -123,7 +123,7 @@ Defines which volumes should be mounted into the container. Each entry is an ind
This is the equivalent of `--mount` flag of Docker. See [Bind mounts documentation of Docker](https://docs.docker.com/engine/storage/volumes/) about the syntax of these entries.
<>
-List of Unix kernel capabilities to be added to the container. This enables a more fine grained version in contrast to give all capabilities to the container with [`privileged`](#property-privileged) = `true`.
+List of Unix kernel capabilities to be added to the container. This enables a more fine-grained version in contrast to give all capabilities to the container with [`privileged`](#property-privileged) = `true`.
Does only work on Unix based systems.
diff --git a/docs/reference/environment/local.md b/docs/reference/environment/local.md
index 2a76107..b9b2950 100644
--- a/docs/reference/environment/local.md
+++ b/docs/reference/environment/local.md
@@ -24,7 +24,7 @@ It can run as the Bifröst user itself, but can also [impersonate](https://en.wi
Users have to fulfill the defined requirements ([`name`](#linux-property-name), [`displayName`](#linux-property-displayName), [`uid`](#linux-property-uid), [`group`](#linux-property-group), [`groups`](#linux-property-groups), [`shell`](#linux-property-shell), [`homeDir`](#linux-property-homeDir) and [`skel`](#linux-property-skel)).
-If a user does not fulfill this requirement they are not eligible for the environment. The environment **can** creates a user ([`createIfAbsent`](#linux-property-createIfAbsent) = `true`) or even updates an existing one ([`updateIfDifferent`](#linux-property-updateIfDifferent) = `true`) to match this requirement. This does not make a lot of sense for [local users](../authorization/local.md); but for [users authorized via OIDC](../authorization/oidc.md) - which usually do not exist locally.
+If a user does not fulfill this requirement they are not eligible for the environment. The environment **can** create a user ([`createIfAbsent`](#linux-property-createIfAbsent) = `true`) or even updates an existing one ([`updateIfDifferent`](#linux-property-updateIfDifferent) = `true`) to match this requirement. This does not make a lot of sense for [local users](../authorization/local.md); but for [users authorized via OIDC](../authorization/oidc.md) - which usually do not exist locally.
See the evaluation matrix of [`createIfAbsent`](#linux-property-createIfAbsent-evaluation) and [`updateIfDifferent`](#linux-property-updateIfDifferent-evaluation) to see the actual reactions of the local environment per users requirement evaluation state.
@@ -147,7 +147,7 @@ This property (together with [`updateIfDifferent`](#linux-property-updateIfDiffe
<>
If an existing user does not match the provided requirements (see below) and the property is `true`, this user is asked to match the requirements.
-This property (together with [`createIfAbsent`](#linux-property-createIfAbsent)) should be `true` if you're using authorizations like [OIDC](../authorization/oidc.md), where the user is not expected to exist locally and you don't want to create each user individually.
+This property (together with [`createIfAbsent`](#linux-property-createIfAbsent)) should be `true` if you're using authorizations like [OIDC](../authorization/oidc.md), where the user is not expected to exist locally, and you don't want to create each user individually.
##### Evaluation {: #linux-property-updateIfDifferent-evaluation}
| [`updateIfDifferent`](#linux-property-updateIfDifferent) | = `false` | = `true` |
diff --git a/docs/usecases.md b/docs/usecases.md
index 7ca7f55..59b5a25 100644
--- a/docs/usecases.md
+++ b/docs/usecases.md
@@ -4,7 +4,7 @@ description: "Bifröst is very flexible in its configuration (see configuration
---
# Use cases
-Bifröst helps IT admins to administer servers much faster, more secure, with more options, and much more flexible than without using Bifröst.
+Bifröst helps IT admins to administer servers much faster, more secure, with more options, and much more flexible than without using Bifröst.
A big advantage of Bifröst is the simple and flexible configuration (see [configuration documentation](reference/configuration.md)). Below, you find some use-cases showing that Bifröst makes the difference:
1. [**Off**-board users within the legally binding 15 minutes timeframe of the organization](#offboard)
@@ -51,7 +51,7 @@ As the users are always authorized by your [Identity Provider (IdP)](https://ope
There is no need to access any of these services directly to remove/de-authorize these users.
-If the [environments are configured accordingly](reference/environment/index.md) (default setting) all of the user's files and processes will be removed/killed automatically, too.
+If the [environments are configured accordingly](reference/environment/index.md) (default setting) all the user's files and processes will be removed/killed automatically, too.
## On-board users within 15 minutes in the organization {: #onboard}
@@ -82,7 +82,7 @@ There is no need to create them somewhere on the server itself. The [OIDC author
There is no need to access any of these services directly to create/authorize these users.
-If the [environments are configured accordingly](reference/environment/index.md) (default setting), all of the user's resources (like the home directory) will be created automatically.
+If the [environments are configured accordingly](reference/environment/index.md) (default setting), all the user's resources (like the home directory) will be created automatically.
## Bastion Host / Jump Host {: #bastion}
@@ -127,7 +127,7 @@ The following cases are usually used:
### Problem
-1. Assume you have a SSH server.
+1. Assume you have an SSH server.
2. Different users should be authorized differently.
3. Different users should run in different [environments](reference/environment/index.md) (one in a local environment with permission A, another with permission B, and a third user in a remote environment).
diff --git a/go.mod b/go.mod
index 3cccd65..c2837e1 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/engity-com/bifroest
go 1.23.0
require (
+ dario.cat/mergo v1.0.1
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
github.com/Masterminds/semver/v3 v3.3.0
github.com/Masterminds/sprig/v3 v3.3.0
@@ -20,7 +21,7 @@ require (
github.com/google/go-containerregistry v0.20.2
github.com/google/go-github/v65 v65.0.0
github.com/google/uuid v1.6.0
- github.com/gwatts/rootcerts v0.0.0-20240701182254-d67b2c3ed211
+ github.com/gwatts/rootcerts v0.0.0-20241101182300-01a0ed5810ce
github.com/mattn/go-zglob v0.0.6
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a
github.com/mr-tron/base58 v1.2.0
@@ -38,58 +39,96 @@ require (
golang.org/x/oauth2 v0.24.0
golang.org/x/sys v0.27.0
gopkg.in/yaml.v3 v3.0.1
+ k8s.io/api v0.31.2
+ k8s.io/apimachinery v0.31.2
+ k8s.io/client-go v0.31.2
)
require (
- dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/containerd/log v0.1.0 // indirect
- github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
- github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/containerd/stargz-snapshotter/estargz v0.16.1 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.1 // indirect
+ github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
+ github.com/go-openapi/jsonpointer v0.21.0 // indirect
+ github.com/go-openapi/jsonreference v0.21.0 // indirect
+ github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/gnostic-models v0.6.8 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
+ github.com/google/gofuzz v1.2.0 // indirect
+ github.com/google/pprof v0.0.0-20241101162523-b92577c0c142 // indirect
+ github.com/gorilla/websocket v1.5.3 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
+ github.com/imdario/mergo v0.3.16 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/term v0.5.0 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
+ github.com/onsi/ginkgo/v2 v2.21.0 // indirect
+ github.com/onsi/gomega v1.35.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
- github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cast v1.7.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.9.0 // indirect
github.com/vbatts/tar-split v0.11.6 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+ github.com/x448/float16 v0.8.4 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
- go.opentelemetry.io/otel v1.31.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect
- go.opentelemetry.io/otel/metric v1.31.0 // indirect
- go.opentelemetry.io/otel/sdk v1.31.0 // indirect
- go.opentelemetry.io/otel/trace v1.31.0 // indirect
- golang.org/x/sync v0.8.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
+ go.opentelemetry.io/otel v1.32.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect
+ go.opentelemetry.io/otel/metric v1.32.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.32.0 // indirect
+ go.opentelemetry.io/otel/trace v1.32.0 // indirect
+ golang.org/x/net v0.31.0 // indirect
+ golang.org/x/sync v0.9.0 // indirect
+ golang.org/x/term v0.26.0 // indirect
+ golang.org/x/text v0.20.0 // indirect
+ golang.org/x/time v0.8.0 // indirect
+ google.golang.org/protobuf v1.35.2 // indirect
+ gopkg.in/inf.v0 v0.9.1 // indirect
+ k8s.io/klog/v2 v2.130.1 // indirect
+ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
+ k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 // indirect
+ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
+ sigs.k8s.io/structured-merge-diff/v4 v4.4.3 // indirect
+ sigs.k8s.io/yaml v1.4.0 // indirect
)
diff --git a/go.sum b/go.sum
index ff78571..109a271 100644
--- a/go.sum
+++ b/go.sum
@@ -18,19 +18,22 @@ github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vS
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
-github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
-github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
+github.com/containerd/stargz-snapshotter/estargz v0.16.1 h1:7YswwU6746cJBN3p3l65JRk3+NZL7bap9Y6E3YeYowk=
+github.com/containerd/stargz-snapshotter/estargz v0.16.1/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
@@ -51,12 +54,16 @@ github.com/echocat/slf4g v1.6.1 h1:nQt0ZivDOsn8dyf3BJaHj1hWTF9MXsoefPUoTbVBI18=
github.com/echocat/slf4g v1.6.1/go.mod h1:YvF/d1TcPvT+/xiHStLHPI4xPT1GGeEmPczn2MSljNA=
github.com/echocat/slf4g/native v1.6.1 h1:g8Y8abLWwFkSQit4JQUvPO1MqpHUiPFZZINjkvPzY50=
github.com/echocat/slf4g/native v1.6.1/go.mod h1:ijM/ey57jUdK7k0uqbhfJJYK6K8DtbEp+HfyDMBO63E=
+github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
+github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
+github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
@@ -69,9 +76,22 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
+github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
@@ -80,14 +100,27 @@ github.com/google/go-github/v65 v65.0.0 h1:pQ7BmO3DZivvFk92geC0jB0q2m3gyn8vnYPgV
github.com/google/go-github/v65 v65.0.0/go.mod h1:DvrqWo5hvsdhJvHd4WyVF9ttANN3BniqjP8uTFMNb60=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20241101162523-b92577c0c142 h1:sAGdeJj0bnMgUNVeUpp6AYlVdCt3/GdI3pGRqsNSQLs=
+github.com/google/pprof v0.0.0-20241101162523-b92577c0c142/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
-github.com/gwatts/rootcerts v0.0.0-20240701182254-d67b2c3ed211 h1:SWrkheqatqllDLJJ1VFDxwAauwXMpYyL0mhR4jjC5nU=
-github.com/gwatts/rootcerts v0.0.0-20240701182254-d67b2c3ed211/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0=
+github.com/gwatts/rootcerts v0.0.0-20241101182300-01a0ed5810ce h1:twIwcQmJuNtw0zFr9S/SQ8PZPIj3+mdhtDSq1ocKXP8=
+github.com/gwatts/rootcerts v0.0.0-20241101182300-01a0ed5810ce/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
@@ -103,6 +136,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A=
github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
@@ -115,14 +150,29 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
+github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/msteinert/pam/v2 v2.0.0 h1:jnObb8MT6jvMbmrUQO5J/puTUjxy7Av+55zVJRJsCyE=
github.com/msteinert/pam/v2 v2.0.0/go.mod h1:KT28NNIcDFf3PcBmNI2mIGO4zZJ+9RSs/At2PB3IDVc=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
+github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
+github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
+github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -137,8 +187,9 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM=
github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY=
-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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@@ -151,10 +202,13 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -173,6 +227,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/xtaci/smux v1.5.31 h1:3ha7sHtH46h85Iv7MfQogxasuRt1KPRhoFB3S4rmHgU=
@@ -182,20 +238,20 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
-go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
-go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4=
-go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
-go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
-go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
-go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
-go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
-go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
+go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
+go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI=
+go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
+go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
+go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
+go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
+go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
+go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -217,8 +273,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
-golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
+golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -226,8 +282,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
-golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
+golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -260,31 +316,53 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
-golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
-golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
+golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
+golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg=
-google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
+google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g=
+google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
-google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
-google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
+google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
+k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0=
+k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk=
+k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw=
+k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
+k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc=
+k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
+k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
+k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 h1:jGnCPejIetjiy2gqaJ5V0NLwTpF4wbQ6cZIItJCSHno=
+k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.3 h1:sCP7Vv3xx/CWIuTPVN38lUPx0uw0lcLfzaiDa8Ja01A=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.3/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/internal/imp/protocol/master.go b/internal/imp/protocol/master.go
index 11721db..9390e9f 100644
--- a/internal/imp/protocol/master.go
+++ b/internal/imp/protocol/master.go
@@ -9,9 +9,9 @@ import (
"sync"
"github.com/engity-com/bifroest/pkg/codec"
+ "github.com/engity-com/bifroest/pkg/common"
"github.com/engity-com/bifroest/pkg/crypto"
"github.com/engity-com/bifroest/pkg/errors"
- "github.com/engity-com/bifroest/pkg/net"
"github.com/engity-com/bifroest/pkg/session"
)
@@ -28,10 +28,8 @@ func NewMaster(_ context.Context, masterPrivateKey crypto.PrivateKey) (*Master,
if err != nil {
return fail(err)
}
- result.tlsDialers.New = func() any {
- return &tls.Dialer{
- Config: result.basicTlsConfigFor(cert, masterPrivateKey.ToSdk()),
- }
+ result.tlsConfigs.New = func() any {
+ return result.basicTlsConfigFor(cert, masterPrivateKey.ToSdk())
}
return result, nil
@@ -40,13 +38,13 @@ func NewMaster(_ context.Context, masterPrivateKey crypto.PrivateKey) (*Master,
type Master struct {
PrivateKey crypto.PrivateKey
- tlsDialers sync.Pool
+ tlsConfigs sync.Pool
}
type Ref interface {
SessionId() session.Id
PublicKey() crypto.PublicKey
- EndpointAddr() net.HostPort
+ Dial(context.Context) (gonet.Conn, error)
}
func (this *Master) Open(_ context.Context, ref Ref) (*MasterSession, error) {
@@ -57,17 +55,26 @@ func (this *Master) Open(_ context.Context, ref Ref) (*MasterSession, error) {
}
func (this *Master) DialContext(ctx context.Context, ref Ref) (gonet.Conn, error) {
- dialer, releaser, err := this.tlsDialerFor(ref)
+ tlsConfig, releaser, err := this.tlsConfigFor(ref)
if err != nil {
return nil, err
}
defer releaser()
- result, err := dialer.DialContext(ctx, "tcp", ref.EndpointAddr().String())
+ success := false
+ rawConn, err := ref.Dial(ctx)
if err != nil {
return nil, err
}
- return result, nil
+ defer common.IgnoreCloseErrorIfFalse(&success, rawConn)
+
+ conn := tls.Client(rawConn, tlsConfig)
+ if err := conn.HandshakeContext(ctx); err != nil {
+ return nil, err
+ }
+
+ success = true
+ return conn, nil
}
func (this *Master) DialContextWithMsgPack(ctx context.Context, ref Ref) (codec.MsgPackConn, error) {
@@ -82,23 +89,23 @@ func (this *Master) DialContextWithMsgPack(ctx context.Context, ref Ref) (codec.
return codec.GetPooledMsgPackConn(conn), nil
}
-func (this *Master) tlsDialerFor(ref Ref) (_ *tls.Dialer, releaser func(), _ error) {
- result := this.tlsDialers.Get().(*tls.Dialer)
+func (this *Master) tlsConfigFor(ref Ref) (_ *tls.Config, releaser func(), _ error) {
+ result := this.tlsConfigs.Get().(*tls.Config)
if pub := ref.PublicKey(); pub != nil {
verifier, err := peerVerifierForPublicKey(pub)
if err != nil {
return nil, nil, err
}
- result.Config.VerifyPeerCertificate = verifier
+ result.VerifyPeerCertificate = verifier
} else if sessionId := ref.SessionId(); !sessionId.IsZero() {
- result.Config.VerifyPeerCertificate = peerVerifierForSessionId(sessionId)
+ result.VerifyPeerCertificate = peerVerifierForSessionId(sessionId)
} else {
return nil, nil, errors.System.Newf("the imp ref provider neither a publicKey nor a sessionId")
}
return result, func() {
- result.Config.VerifyPeerCertificate = alwaysRejectPeerVerifier
- this.tlsDialers.Put(result)
+ result.VerifyPeerCertificate = alwaysRejectPeerVerifier
+ this.tlsConfigs.Put(result)
}, nil
}
diff --git a/pkg/alternatives/binaries.go b/pkg/alternatives/provider.go
similarity index 71%
rename from pkg/alternatives/binaries.go
rename to pkg/alternatives/provider.go
index b2bca94..a9cb6a4 100644
--- a/pkg/alternatives/binaries.go
+++ b/pkg/alternatives/provider.go
@@ -8,29 +8,45 @@ import (
goos "os"
"path/filepath"
+ "github.com/docker/docker/errdefs"
log "github.com/echocat/slf4g"
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/v1/daemon"
"github.com/engity-com/bifroest/pkg/common"
"github.com/engity-com/bifroest/pkg/configuration"
"github.com/engity-com/bifroest/pkg/errors"
+ "github.com/engity-com/bifroest/pkg/oci/images"
"github.com/engity-com/bifroest/pkg/sys"
)
type Provider interface {
io.Closer
FindBinaryFor(ctx context.Context, os sys.Os, arch sys.Arch) (string, error)
+ FindOciImageFor(ctx context.Context, os sys.Os, arch sys.Arch, opts FindOciImageOpts) (string, error)
+}
+
+type FindOciImageOpts struct {
+ Local bool
+ Force bool
}
func NewProvider(_ context.Context, version sys.Version, conf *configuration.Alternatives) (Provider, error) {
+ ib, err := images.NewBuilder()
+ if err != nil {
+ return nil, err
+ }
return &provider{
- conf: conf,
- version: version,
+ conf: conf,
+ version: version,
+ imagesBuilder: ib,
}, nil
}
type provider struct {
- conf *configuration.Alternatives
- version sys.Version
+ conf *configuration.Alternatives
+ version sys.Version
+ imagesBuilder *images.Builder
Logger log.Logger
}
@@ -129,6 +145,69 @@ func (this *provider) FindBinaryFor(ctx context.Context, hostOs sys.Os, hostArch
return fn, nil
}
+func (this *provider) FindOciImageFor(ctx context.Context, os sys.Os, arch sys.Arch, opts FindOciImageOpts) (_ string, rErr error) {
+ fail := func(err error) (string, error) {
+ return "", errors.System.Newf("cannot find oci image for %v/%v: %w", os, arch, err)
+ }
+ failf := func(msg string, args ...any) (string, error) {
+ return fail(errors.System.Newf(msg, args...))
+ }
+ version := this.version.Version()
+
+ if !opts.Local {
+ return "ghcr.io/engity-com/bifroest:generic-" + version, nil
+ }
+
+ tag, err := name.NewTag("local/bifroest:generic-" + version)
+ if err != nil {
+ return fail(err)
+ }
+
+ if !opts.Force {
+ if _, err := daemon.Image(tag, daemon.WithContext(ctx)); errdefs.IsNotFound(err) {
+ // This is Ok, we'll continue to build it...
+ } else if err != nil {
+ return fail(err)
+ } else {
+ return tag.String(), nil
+ }
+ }
+
+ sourceBinaryLocation, err := this.FindBinaryFor(ctx, os, arch)
+ if err != nil {
+ return fail(err)
+ }
+
+ targetBinaryLocation := sys.BifroestBinaryLocation(os)
+ if len(targetBinaryLocation) == 0 {
+ return failf("cannot resolve Bifröst's binary location for os %v", os)
+ }
+
+ img, err := this.imagesBuilder.Build(ctx, images.BuildRequest{
+ From: images.ImageMinimal,
+ Os: os,
+ Arch: arch,
+ BifroestBinarySource: sourceBinaryLocation,
+ EntryPoint: []string{targetBinaryLocation},
+ Cmd: []string{},
+ ExposedPorts: map[string]struct{}{
+ "22/tcp": {},
+ },
+ AddDummyConfiguration: true,
+ AddSkeletonStructure: true,
+ })
+ if err != nil {
+ return fail(err)
+ }
+ defer common.KeepCloseError(&rErr, img)
+
+ if _, err := daemon.Write(tag, img, daemon.WithContext(ctx)); err != nil {
+ return fail(err)
+ }
+
+ return tag.String(), nil
+}
+
func (this *provider) alternativesLocationFor(os sys.Os, arch sys.Arch) (string, error) {
fail := func(err error) (string, error) {
return "", err
diff --git a/pkg/authorization/facade-authorizer.go b/pkg/authorization/facade-authorizer.go
index b3380b4..2b441c4 100644
--- a/pkg/authorization/facade-authorizer.go
+++ b/pkg/authorization/facade-authorizer.go
@@ -92,6 +92,7 @@ func (this *AuthorizerFacade) RestoreFromSession(ctx context.Context, sess sessi
func (this *AuthorizerFacade) Close() (rErr error) {
defer func() { this.entries = nil }()
for _, candidate := range this.entries {
+ //goland:noinspection GoDeferInLoop
defer common.KeepCloseError(&rErr, candidate)
}
return nil
diff --git a/pkg/configuration/authorization-simple-entry.go b/pkg/configuration/authorization-simple-entry.go
index 815b312..250c543 100644
--- a/pkg/configuration/authorization-simple-entry.go
+++ b/pkg/configuration/authorization-simple-entry.go
@@ -59,7 +59,7 @@ func (this *AuthorizationSimpleEntry) Validate() error {
return err
}
if f := this.PasswordFile; !f.IsZero() {
- if pw, err := this.PasswordFile.GetPassword(); pw.IsZero() {
+ if pw, err := this.PasswordFile.GetPassword(); pw == nil || pw.IsZero() {
decoded, encoded, err := t.Generate(nil)
if err != nil {
return errors.System.Newf("cannot generate new password for file %v: %w", f, err)
diff --git a/pkg/configuration/context-mode.go b/pkg/configuration/context-mode.go
new file mode 100644
index 0000000..e6f5146
--- /dev/null
+++ b/pkg/configuration/context-mode.go
@@ -0,0 +1,90 @@
+package configuration
+
+import (
+ "fmt"
+
+ "github.com/engity-com/bifroest/pkg/errors"
+)
+
+type ContextMode uint8
+
+const (
+ ContextModeOnline ContextMode = iota
+ ContextModeOffline
+ ContextModeDebug
+)
+
+var (
+ contextModeToName = map[ContextMode]string{
+ ContextModeOnline: "online",
+ ContextModeOffline: "offline",
+ ContextModeDebug: "debug",
+ }
+ nameToContextMode = func(in map[ContextMode]string) map[string]ContextMode {
+ result := make(map[string]ContextMode, len(in))
+ for k, v := range in {
+ result[v] = k
+ }
+ return result
+ }(contextModeToName)
+)
+
+func (this ContextMode) IsZero() bool {
+ return false
+}
+
+func (this ContextMode) MarshalText() (text []byte, err error) {
+ v, ok := contextModeToName[this]
+ if !ok {
+ return nil, errors.Config.Newf("illegal context-mode: %d", this)
+ }
+ return []byte(v), nil
+}
+
+func (this ContextMode) String() string {
+ v, ok := contextModeToName[this]
+ if !ok {
+ return fmt.Sprintf("illegal-context-mode-%d", this)
+ }
+ return v
+}
+
+func (this *ContextMode) UnmarshalText(text []byte) error {
+ v, ok := nameToContextMode[string(text)]
+ if !ok {
+ return errors.Config.Newf("illegal context-mode: %s", string(text))
+ }
+ *this = v
+ return nil
+}
+
+func (this *ContextMode) Set(text string) error {
+ return this.UnmarshalText([]byte(text))
+}
+
+func (this ContextMode) Validate() error {
+ _, err := this.MarshalText()
+ return err
+}
+
+func (this ContextMode) IsEqualTo(other any) bool {
+ if other == nil {
+ return false
+ }
+ switch v := other.(type) {
+ case ContextMode:
+ return this.isEqualTo(&v)
+ case *ContextMode:
+ return this.isEqualTo(v)
+ default:
+ return false
+ }
+}
+
+func (this ContextMode) isEqualTo(other *ContextMode) bool {
+ return this == *other
+}
+
+func (this ContextMode) Clone() ContextMode {
+ return this
+}
diff --git a/pkg/configuration/environment-kubernetes.go b/pkg/configuration/environment-kubernetes.go
new file mode 100644
index 0000000..01ce133
--- /dev/null
+++ b/pkg/configuration/environment-kubernetes.go
@@ -0,0 +1,245 @@
+package configuration
+
+import (
+ "time"
+
+ "gopkg.in/yaml.v3"
+
+ "github.com/engity-com/bifroest/pkg/kubernetes"
+ "github.com/engity-com/bifroest/pkg/sys"
+ "github.com/engity-com/bifroest/pkg/template"
+)
+
+var (
+ DefaultEnvironmentKubernetesLoginAllowed = template.BoolOf(true)
+
+ DefaultEnvironmentKubernetesConfig = kubernetes.MustNewKubeconfig("")
+ DefaultEnvironmentKubernetesContext = ""
+
+ DefaultEnvironmentKubernetesName = template.MustNewString("bifroest-{{.session.id}}")
+ DefaultEnvironmentKubernetesNamespace = template.MustNewString("")
+ DefaultEnvironmentKubernetesOs = sys.OsLinux
+ DefaultEnvironmentKubernetesServiceAccount = template.MustNewString("")
+ DefaultEnvironmentKubernetesImage = template.MustNewString("alpine")
+ DefaultEnvironmentKubernetesImagePullPolicy = PullPolicyIfAbsent
+ DefaultEnvironmentKubernetesImagePullCredentials = template.MustNewString("")
+ DefaultEnvironmentKubernetesImageContextMode = ContextModeOnline
+ DefaultEnvironmentKubernetesReadyTimeout = template.DurationOf(5 * time.Minute)
+
+ DefaultEnvironmentKubernetesCapabilities = template.MustNewStrings()
+ DefaultEnvironmentKubernetesPrivileged = template.BoolOf(false)
+ DefaultEnvironmentKubernetesDnsServers = template.MustNewStrings()
+ DefaultEnvironmentKubernetesDnsSearch = template.MustNewStrings()
+ DefaultEnvironmentKubernetesShellCommand = template.MustNewStrings()
+ DefaultEnvironmentKubernetesExecCommand = template.MustNewStrings()
+ DefaultEnvironmentKubernetesSftpCommand = template.MustNewStrings()
+ DefaultEnvironmentKubernetesDirectory = template.MustNewString("")
+ DefaultEnvironmentKubernetesUser = template.MustNewString("")
+
+ DefaultEnvironmentKubernetesBanner = template.MustNewString("")
+ DefaultEnvironmentKubernetesPortForwardingAllowed = template.BoolOf(true)
+
+ DefaultEnvironmentKubernetesCleanOrphan = template.BoolOf(true)
+
+ _ = RegisterEnvironmentV(func() EnvironmentV {
+ return &EnvironmentKubernetes{}
+ })
+)
+
+type EnvironmentKubernetes struct {
+ LoginAllowed template.Bool `yaml:"loginAllowed,omitempty"`
+
+ Config kubernetes.Kubeconfig `yaml:"config,omitempty"`
+ Context string `yaml:"context,omitempty"`
+
+ Name template.String `yaml:"name"`
+ Namespace template.String `yaml:"namespace,omitempty"`
+ Os sys.Os `yaml:"os"`
+ ServiceAccount template.String `yaml:"serviceAccount,omitempty"`
+ Image template.String `yaml:"image"`
+ ImagePullPolicy PullPolicy `yaml:"imagePullPolicy,omitempty"`
+ ImagePullCredentials template.String `yaml:"imagePullCredentials,omitempty"`
+ ImageContextMode ContextMode `yaml:"imageContextMode,omitempty"`
+ ReadyTimeout template.Duration `yaml:"readyTimeout,omitempty"`
+ Capabilities template.Strings `yaml:"capabilities,omitempty"`
+ Privileged template.Bool `yaml:"privileged,omitempty"`
+ DnsServers template.Strings `yaml:"dnsServers,omitempty"`
+ DnsSearch template.Strings `yaml:"dnsSearch,omitempty"`
+
+ ShellCommand template.Strings `yaml:"shellCommand,omitempty"`
+ ExecCommand template.Strings `yaml:"execCommand,omitempty"`
+ SftpCommand template.Strings `yaml:"sftpCommand,omitempty"`
+ Directory template.String `yaml:"directory"`
+ User template.String `yaml:"user,omitempty"`
+
+ Banner template.String `yaml:"banner,omitempty"`
+
+ PortForwardingAllowed template.Bool `yaml:"portForwardingAllowed,omitempty"`
+
+ CleanOrphan template.Bool `yaml:"cleanOrphan,omitempty"`
+}
+
+func (this *EnvironmentKubernetes) SetDefaults() error {
+ return setDefaults(this,
+ fixedDefault("loginAllowed", func(v *EnvironmentKubernetes) *template.Bool { return &v.LoginAllowed }, DefaultEnvironmentKubernetesLoginAllowed),
+
+ fixedDefault("config", func(v *EnvironmentKubernetes) *kubernetes.Kubeconfig { return &v.Config }, DefaultEnvironmentKubernetesConfig),
+ fixedDefault("context", func(v *EnvironmentKubernetes) *string { return &v.Context }, DefaultEnvironmentKubernetesContext),
+
+ fixedDefault("name", func(v *EnvironmentKubernetes) *template.String { return &v.Name }, DefaultEnvironmentKubernetesName),
+ fixedDefault("namespace", func(v *EnvironmentKubernetes) *template.String { return &v.Namespace }, DefaultEnvironmentKubernetesNamespace),
+ fixedDefault("os", func(v *EnvironmentKubernetes) *sys.Os { return &v.Os }, DefaultEnvironmentKubernetesOs),
+ fixedDefault("serviceAccount", func(v *EnvironmentKubernetes) *template.String { return &v.ServiceAccount }, DefaultEnvironmentKubernetesServiceAccount),
+ fixedDefault("image", func(v *EnvironmentKubernetes) *template.String { return &v.Image }, DefaultEnvironmentKubernetesImage),
+ fixedDefault("imagePullPolicy", func(v *EnvironmentKubernetes) *PullPolicy { return &v.ImagePullPolicy }, DefaultEnvironmentKubernetesImagePullPolicy),
+ fixedDefault("imagePullCredentials", func(v *EnvironmentKubernetes) *template.String { return &v.ImagePullCredentials }, DefaultEnvironmentKubernetesImagePullCredentials),
+ fixedDefault("imageContextMode", func(v *EnvironmentKubernetes) *ContextMode { return &v.ImageContextMode }, DefaultEnvironmentKubernetesImageContextMode),
+ fixedDefault("readyTimeout", func(v *EnvironmentKubernetes) *template.Duration { return &v.ReadyTimeout }, DefaultEnvironmentKubernetesReadyTimeout),
+ fixedDefault("capabilities", func(v *EnvironmentKubernetes) *template.Strings { return &v.Capabilities }, DefaultEnvironmentKubernetesCapabilities),
+ fixedDefault("privileged", func(v *EnvironmentKubernetes) *template.Bool { return &v.Privileged }, DefaultEnvironmentKubernetesPrivileged),
+ fixedDefault("dnsServers", func(v *EnvironmentKubernetes) *template.Strings { return &v.DnsServers }, DefaultEnvironmentKubernetesDnsServers),
+ fixedDefault("dnsSearch", func(v *EnvironmentKubernetes) *template.Strings { return &v.DnsSearch }, DefaultEnvironmentKubernetesDnsSearch),
+
+ fixedDefault("shellCommand", func(v *EnvironmentKubernetes) *template.Strings { return &v.ShellCommand }, DefaultEnvironmentKubernetesShellCommand),
+ fixedDefault("execCommand", func(v *EnvironmentKubernetes) *template.Strings { return &v.ExecCommand }, DefaultEnvironmentKubernetesExecCommand),
+ fixedDefault("sftpCommand", func(v *EnvironmentKubernetes) *template.Strings { return &v.SftpCommand }, DefaultEnvironmentKubernetesSftpCommand),
+ fixedDefault("directory", func(v *EnvironmentKubernetes) *template.String { return &v.Directory }, DefaultEnvironmentKubernetesDirectory),
+ fixedDefault("user", func(v *EnvironmentKubernetes) *template.String { return &v.User }, DefaultEnvironmentKubernetesUser),
+
+ fixedDefault("banner", func(v *EnvironmentKubernetes) *template.String { return &v.Banner }, DefaultEnvironmentKubernetesBanner),
+
+ fixedDefault("portForwardingAllowed", func(v *EnvironmentKubernetes) *template.Bool { return &v.PortForwardingAllowed }, DefaultEnvironmentKubernetesPortForwardingAllowed),
+
+ fixedDefault("cleanOrphan", func(v *EnvironmentKubernetes) *template.Bool { return &v.CleanOrphan }, DefaultEnvironmentKubernetesCleanOrphan),
+ )
+}
+
+func (this *EnvironmentKubernetes) Trim() error {
+ return trim(this,
+ noopTrim[EnvironmentKubernetes]("loginAllowed"),
+
+ noopTrim[EnvironmentKubernetes]("config"),
+ noopTrim[EnvironmentKubernetes]("context"),
+
+ noopTrim[EnvironmentKubernetes]("name"),
+ noopTrim[EnvironmentKubernetes]("namespace"),
+ noopTrim[EnvironmentKubernetes]("os"),
+ noopTrim[EnvironmentKubernetes]("serviceAccount"),
+ noopTrim[EnvironmentKubernetes]("image"),
+ noopTrim[EnvironmentKubernetes]("imagePullPolicy"),
+ noopTrim[EnvironmentKubernetes]("imagePullCredentials"),
+ noopTrim[EnvironmentKubernetes]("imageContextMode"),
+ noopTrim[EnvironmentKubernetes]("readyTimeout"),
+ noopTrim[EnvironmentKubernetes]("capabilities"),
+ noopTrim[EnvironmentKubernetes]("privileged"),
+ noopTrim[EnvironmentKubernetes]("dnsServers"),
+ noopTrim[EnvironmentKubernetes]("dnsSearch"),
+ noopTrim[EnvironmentKubernetes]("shellCommand"),
+ noopTrim[EnvironmentKubernetes]("execCommand"),
+ noopTrim[EnvironmentKubernetes]("sftpCommand"),
+ noopTrim[EnvironmentKubernetes]("directory"),
+ noopTrim[EnvironmentKubernetes]("user"),
+
+ noopTrim[EnvironmentKubernetes]("banner"),
+
+ noopTrim[EnvironmentKubernetes]("portForwardingAllowed"),
+
+ noopTrim[EnvironmentKubernetes]("cleanOrphan"),
+ )
+}
+
+func (this *EnvironmentKubernetes) Validate() error {
+ return validate(this,
+ func(v *EnvironmentKubernetes) (string, validator) { return "loginAllowed", &v.LoginAllowed },
+
+ func(v *EnvironmentKubernetes) (string, validator) { return "config", &v.Config },
+ noopValidate[EnvironmentKubernetes]("context"),
+
+ func(v *EnvironmentKubernetes) (string, validator) { return "name", &v.Name },
+ notZeroValidate("name", func(v *EnvironmentKubernetes) *template.String { return &v.Name }),
+ func(v *EnvironmentKubernetes) (string, validator) { return "namespace", &v.Namespace },
+ func(v *EnvironmentKubernetes) (string, validator) { return "os", &v.Os },
+ func(v *EnvironmentKubernetes) (string, validator) { return "serviceAccount", &v.ServiceAccount },
+ func(v *EnvironmentKubernetes) (string, validator) { return "image", &v.Image },
+ notZeroValidate("image", func(v *EnvironmentKubernetes) *template.String { return &v.Image }),
+ func(v *EnvironmentKubernetes) (string, validator) { return "imagePullPolicy", &v.ImagePullPolicy },
+ func(v *EnvironmentKubernetes) (string, validator) {
+ return "imagePullCredentials", &v.ImagePullCredentials
+ },
+ func(v *EnvironmentKubernetes) (string, validator) { return "imageContextMode", &v.ImageContextMode },
+ func(v *EnvironmentKubernetes) (string, validator) { return "readyTimeout", &v.ReadyTimeout },
+ func(v *EnvironmentKubernetes) (string, validator) { return "capabilities", &v.Capabilities },
+ func(v *EnvironmentKubernetes) (string, validator) { return "privileged", &v.Privileged },
+ func(v *EnvironmentKubernetes) (string, validator) { return "dnsServers", &v.DnsServers },
+ func(v *EnvironmentKubernetes) (string, validator) { return "dnsSearch", &v.DnsSearch },
+ func(v *EnvironmentKubernetes) (string, validator) { return "shellCommand", &v.ShellCommand },
+ func(v *EnvironmentKubernetes) (string, validator) { return "execCommand", &v.ExecCommand },
+ func(v *EnvironmentKubernetes) (string, validator) { return "sftpCommand", &v.SftpCommand },
+ func(v *EnvironmentKubernetes) (string, validator) { return "directory", &v.Directory },
+ func(v *EnvironmentKubernetes) (string, validator) { return "user", &v.User },
+
+ func(v *EnvironmentKubernetes) (string, validator) { return "banner", &v.Banner },
+
+ func(v *EnvironmentKubernetes) (string, validator) {
+ return "portForwardingAllowed", &v.PortForwardingAllowed
+ },
+
+ func(v *EnvironmentKubernetes) (string, validator) { return "cleanOrphan", &v.CleanOrphan },
+ )
+}
+
+func (this *EnvironmentKubernetes) UnmarshalYAML(node *yaml.Node) error {
+ return unmarshalYAML(this, node, func(target *EnvironmentKubernetes, node *yaml.Node) error {
+ type raw EnvironmentKubernetes
+ return node.Decode((*raw)(target))
+ })
+}
+
+func (this EnvironmentKubernetes) IsEqualTo(other any) bool {
+ if other == nil {
+ return false
+ }
+ switch v := other.(type) {
+ case EnvironmentKubernetes:
+ return this.isEqualTo(&v)
+ case *EnvironmentKubernetes:
+ return this.isEqualTo(v)
+ default:
+ return false
+ }
+}
+
+func (this EnvironmentKubernetes) isEqualTo(other *EnvironmentKubernetes) bool {
+ return isEqual(&this.LoginAllowed, &other.LoginAllowed) &&
+ isEqual(&this.Config, &other.Config) &&
+ this.Context == other.Context &&
+ isEqual(&this.Name, &other.Name) &&
+ isEqual(&this.Namespace, &other.Namespace) &&
+ isEqual(&this.Os, &other.Os) &&
+ isEqual(&this.ServiceAccount, &other.ServiceAccount) &&
+ isEqual(&this.Image, &other.Image) &&
+ isEqual(&this.ImagePullPolicy, &other.ImagePullPolicy) &&
+ isEqual(&this.ImagePullCredentials, &other.ImagePullCredentials) &&
+ isEqual(&this.ImageContextMode, &other.ImageContextMode) &&
+ isEqual(&this.ReadyTimeout, &other.ReadyTimeout) &&
+ isEqual(&this.Capabilities, &other.Capabilities) &&
+ isEqual(&this.Privileged, &other.Privileged) &&
+ isEqual(&this.DnsServers, &other.DnsServers) &&
+ isEqual(&this.DnsSearch, &other.DnsSearch) &&
+ isEqual(&this.ShellCommand, &other.ShellCommand) &&
+ isEqual(&this.ExecCommand, &other.ExecCommand) &&
+ isEqual(&this.SftpCommand, &other.SftpCommand) &&
+ isEqual(&this.Directory, &other.Directory) &&
+ isEqual(&this.User, &other.User) &&
+ isEqual(&this.Banner, &other.Banner) &&
+ isEqual(&this.PortForwardingAllowed, &other.PortForwardingAllowed) &&
+ isEqual(&this.CleanOrphan, &other.CleanOrphan)
+}
+
+func (this EnvironmentKubernetes) Types() []string {
+ return []string{"kubernetes"}
+}
+
+func (this EnvironmentKubernetes) FeatureFlags() []string {
+ return []string{"kubernetes"}
+}
diff --git a/pkg/configuration/flow-key.go b/pkg/configuration/flow-key.go
index 5adddb6..18acc05 100644
--- a/pkg/configuration/flow-key.go
+++ b/pkg/configuration/flow-key.go
@@ -37,9 +37,9 @@ func (this FlowName) Validate() error {
return fmt.Errorf("illegal flow key: empty")
}
for _, c := range string(this) {
- if (c >= 'a' && 'z' <= c) ||
- (c >= 'A' && 'Z' <= c) ||
- (c >= '0' && '9' <= c) ||
+ if (c >= 'a' && 'z' >= c) ||
+ (c >= 'A' && 'Z' >= c) ||
+ (c >= '0' && '9' >= c) ||
c == '-' || c == '.' {
// Ok
} else {
diff --git a/pkg/configuration/pull-policy.go b/pkg/configuration/pull-policy.go
index 2e07a9d..c784e38 100644
--- a/pkg/configuration/pull-policy.go
+++ b/pkg/configuration/pull-policy.go
@@ -11,12 +11,14 @@ type PullPolicy uint8
const (
PullPolicyIfAbsent PullPolicy = iota
PullPolicyAlways
+ PullPolicyNever
)
var (
pullPolicyToName = map[PullPolicy]string{
PullPolicyIfAbsent: "ifAbsent",
PullPolicyAlways: "always",
+ PullPolicyNever: "never",
}
nameToPullPolicy = func(in map[PullPolicy]string) map[string]PullPolicy {
result := make(map[string]PullPolicy, len(in))
diff --git a/pkg/configuration/configuration-ref.go b/pkg/configuration/ref.go
similarity index 54%
rename from pkg/configuration/configuration-ref.go
rename to pkg/configuration/ref.go
index 0b18036..a7ce5c4 100644
--- a/pkg/configuration/configuration-ref.go
+++ b/pkg/configuration/ref.go
@@ -6,25 +6,25 @@ import (
"github.com/engity-com/bifroest/pkg/errors"
)
-type ConfigurationRef struct {
+type Ref struct {
v Configuration
fn string
}
-func (this ConfigurationRef) IsZero() bool {
+func (this Ref) IsZero() bool {
return len(this.fn) == 0
}
-func (this ConfigurationRef) MarshalText() (text []byte, err error) {
+func (this Ref) MarshalText() (text []byte, err error) {
return []byte(this.String()), nil
}
-func (this ConfigurationRef) String() string {
+func (this Ref) String() string {
return this.fn
}
-func (this *ConfigurationRef) UnmarshalText(text []byte) error {
- buf := ConfigurationRef{
+func (this *Ref) UnmarshalText(text []byte) error {
+ buf := Ref{
fn: string(text),
}
@@ -38,19 +38,19 @@ func (this *ConfigurationRef) UnmarshalText(text []byte) error {
return nil
}
-func (this *ConfigurationRef) Set(text string) error {
+func (this *Ref) Set(text string) error {
return this.UnmarshalText([]byte(text))
}
-func (this *ConfigurationRef) Get() *Configuration {
+func (this *Ref) Get() *Configuration {
return &this.v
}
-func (this *ConfigurationRef) GetFilename() string {
+func (this *Ref) GetFilename() string {
return this.fn
}
-func (this *ConfigurationRef) MakeAbsolute() error {
+func (this *Ref) MakeAbsolute() error {
abs, err := filepath.Abs(this.fn)
if err != nil {
return errors.Config.Newf("canont make this configuration file reference absolute: %w", err)
@@ -58,20 +58,20 @@ func (this *ConfigurationRef) MakeAbsolute() error {
return this.Set(abs)
}
-func (this ConfigurationRef) IsEqualTo(other any) bool {
+func (this Ref) IsEqualTo(other any) bool {
if other == nil {
return false
}
switch v := other.(type) {
- case ConfigurationRef:
+ case Ref:
return this.isEqualTo(&v)
- case *ConfigurationRef:
+ case *Ref:
return this.isEqualTo(v)
default:
return false
}
}
-func (this ConfigurationRef) isEqualTo(other *ConfigurationRef) bool {
+func (this Ref) isEqualTo(other *Ref) bool {
return this.fn == other.fn
}
diff --git a/pkg/configuration/ssh.go b/pkg/configuration/ssh.go
index 02b01cd..67be114 100644
--- a/pkg/configuration/ssh.go
+++ b/pkg/configuration/ssh.go
@@ -12,7 +12,7 @@ import (
var (
// DefaultSshAddresses is the default setting for Ssh.Addresses.
- DefaultSshAddresses = []net.NetAddress{net.MustNewNetAddress(":22")}
+ DefaultSshAddresses = []net.Address{net.MustNewAddress(":22")}
// DefaultSshIdleTimeout is the default setting for Ssh.IdleTimeout.
DefaultSshIdleTimeout = common.DurationOf(10 * time.Minute)
diff --git a/pkg/crypto/authorized-keys_test.go b/pkg/crypto/authorized-keys_test.go
index 5c78a16..64249dc 100644
--- a/pkg/crypto/authorized-keys_test.go
+++ b/pkg/crypto/authorized-keys_test.go
@@ -15,6 +15,7 @@ import (
)
//nolint:golint,unused
+//goland:noinspection ALL
var (
dsa1Pub, dsa1Fn = mustSshPublicKey("dsa-1")
ecdsa1Pub, ecdsa1Fn = mustSshPublicKey("ecdsa-1")
diff --git a/pkg/environment/docker-repository.go b/pkg/environment/docker-repository.go
index 8931db7..6139ae3 100644
--- a/pkg/environment/docker-repository.go
+++ b/pkg/environment/docker-repository.go
@@ -5,6 +5,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
+ gonet "net"
"path/filepath"
"strconv"
"strings"
@@ -40,9 +41,6 @@ var (
)
const (
- BifroestUnixBinaryMountTarget = `/usr/bin/bifroest`
- BifroestWindowsBinaryMountTarget = `C:\Program Files\Engity\Bifroest\bifroest.exe`
-
DockerLabelPrefix = "org.engity.bifroest/"
DockerLabelFlow = DockerLabelPrefix + "flow"
DockerLabelSessionId = DockerLabelPrefix + "session-id"
@@ -73,6 +71,8 @@ type DockerRepository struct {
sessionIdMutex common.KeyedMutex[session.Id]
activeInstances sync.Map
+
+ rawDialer gonet.Dialer
}
func NewDockerRepository(ctx context.Context, flow configuration.FlowName, conf *configuration.EnvironmentDocker, ap alternatives.Provider, i imp.Imp) (*DockerRepository, error) {
@@ -192,7 +192,7 @@ func (this *DockerRepository) createContainerBy(req Request, sess session.Sessio
}
success := false
cr, err := this.apiClient.ContainerCreate(req.Context(), config, hostConfig, networkingConfig, nil, "")
- if this.isNoSuchImageError(err) && this.conf.ImagePullPolicy != configuration.PullPolicyAlways {
+ if this.isNoSuchImageError(err) && this.conf.ImagePullPolicy != configuration.PullPolicyAlways && this.conf.ImagePullPolicy != configuration.PullPolicyNever {
if err := this.pullImage(req, config.Image); err != nil {
return failf(errors.System, "cannot pull container image %s: %w", config.Image, err)
}
@@ -351,15 +351,13 @@ func (this *DockerRepository) resolveContainerConfig(req Request, sess session.S
return failf("cannot evaluate image: %w", err)
}
result.Entrypoint = strslice.StrSlice{}
- switch this.hostOs {
- case sys.OsWindows:
- result.Cmd = []string{BifroestWindowsBinaryMountTarget}
- case sys.OsLinux:
- result.User = "root"
- result.Cmd = []string{BifroestUnixBinaryMountTarget}
- default:
+ result.Cmd = []string{sys.BifroestBinaryLocation(this.hostOs)}
+ if len(result.Cmd[0]) == 0 {
return failf("cannot resolve target path for host %s/%s", this.hostOs, this.hostArch)
}
+ if this.hostOs == sys.OsLinux {
+ result.User = "root"
+ }
result.Cmd = append(result.Cmd, `imp`, `--log.colorMode=always`)
if this.defaultLogLevelName != "" {
@@ -434,13 +432,8 @@ func (this *DockerRepository) resolveHostConfig(req Request) (_ *container.HostC
if err != nil {
return failf("cannot resolve full imp binary path: %w", err)
}
- var targetPath string
- switch this.hostOs {
- case sys.OsWindows:
- targetPath = BifroestWindowsBinaryMountTarget
- case sys.OsLinux:
- targetPath = BifroestUnixBinaryMountTarget
- default:
+ targetPath := sys.BifroestBinaryLocation(this.hostOs)
+ if targetPath == "" {
return failf("cannot resolve target path for host %s/%s", this.hostOs, this.hostArch)
}
result.Mounts = append(result.Mounts, mount.Mount{
@@ -514,12 +507,8 @@ func (this *DockerRepository) resolveEncodedSftpCommand(req Request) (string, er
return failf("cannot evaluate sftpCommand: %w", err)
}
if len(v) == 0 {
- switch this.hostOs {
- case sys.OsWindows:
- v = []string{BifroestWindowsBinaryMountTarget, `sftp-server`}
- case sys.OsLinux:
- v = []string{BifroestUnixBinaryMountTarget, `sftp-server`}
- default:
+ v = []string{sys.BifroestBinaryLocation(this.hostOs), `sftp-server`}
+ if len(v[0]) == 0 {
return failf("sftpCommand was not defined for docker environment and default cannot be resolved for %s/%s", this.hostOs, this.hostArch)
}
}
diff --git a/pkg/environment/docker.go b/pkg/environment/docker.go
index 64526b1..14db7eb 100644
--- a/pkg/environment/docker.go
+++ b/pkg/environment/docker.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
+ gonet "net"
"sync/atomic"
"syscall"
"time"
@@ -53,8 +54,8 @@ func (this *docker) PublicKey() crypto.PublicKey {
return nil
}
-func (this *docker) EndpointAddr() net.HostPort {
- return this.impBinding
+func (this *docker) Dial(ctx context.Context) (gonet.Conn, error) {
+ return this.repository.rawDialer.DialContext(ctx, "tcp", this.impBinding.String())
}
func (this *DockerRepository) new(ctx context.Context, container *types.Container, logger log.Logger) (*docker, error) {
diff --git a/pkg/environment/facade-repository.go b/pkg/environment/facade-repository.go
index 667afe8..2d8e69f 100644
--- a/pkg/environment/facade-repository.go
+++ b/pkg/environment/facade-repository.go
@@ -74,6 +74,7 @@ func (this *RepositoryFacade) FindBySession(ctx context.Context, sess session.Se
func (this *RepositoryFacade) Close() (rErr error) {
for _, entity := range this.entries {
+ //goland:noinspection GoDeferInLoop
defer common.KeepCloseError(&rErr, entity)
}
return nil
diff --git a/pkg/environment/kubernetes-repository.go b/pkg/environment/kubernetes-repository.go
new file mode 100644
index 0000000..96a7e41
--- /dev/null
+++ b/pkg/environment/kubernetes-repository.go
@@ -0,0 +1,903 @@
+package environment
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+ "unsafe"
+
+ "github.com/docker/docker/api/types/registry"
+ "github.com/docker/docker/api/types/strslice"
+ "github.com/docker/docker/errdefs"
+ "github.com/echocat/slf4g"
+ "github.com/echocat/slf4g/level"
+ glssh "github.com/gliderlabs/ssh"
+ v1 "k8s.io/api/core/v1"
+ kerrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/util/intstr"
+ v2 "k8s.io/client-go/kubernetes/typed/core/v1"
+
+ "github.com/engity-com/bifroest/pkg/alternatives"
+ "github.com/engity-com/bifroest/pkg/common"
+ "github.com/engity-com/bifroest/pkg/configuration"
+ "github.com/engity-com/bifroest/pkg/errors"
+ "github.com/engity-com/bifroest/pkg/imp"
+ bkp "github.com/engity-com/bifroest/pkg/kubernetes"
+ "github.com/engity-com/bifroest/pkg/session"
+ "github.com/engity-com/bifroest/pkg/sys"
+)
+
+var (
+ _ = RegisterRepository(NewKubernetesRepository)
+)
+
+const (
+ KubernetesLabelPrefix = "org.engity.bifroest/"
+ KubernetesLabelFlow = KubernetesLabelPrefix + "flow"
+ KubernetesLabelSessionId = KubernetesLabelPrefix + "session-id"
+
+ KubernetesAnnotationPrefix = KubernetesLabelPrefix
+ KubernetesAnnotationCreatedRemoteUser = KubernetesAnnotationPrefix + "created-remote-user"
+ KubernetesAnnotationCreatedRemoteHost = KubernetesAnnotationPrefix + "created-remote-host"
+ KubernetesAnnotationShellCommand = KubernetesAnnotationPrefix + "shellCommand"
+ KubernetesAnnotationExecCommand = KubernetesAnnotationPrefix + "execCommand"
+ KubernetesAnnotationSftpCommand = KubernetesAnnotationPrefix + "sftpCommand"
+ KubernetesAnnotationUser = KubernetesAnnotationPrefix + "user"
+ KubernetesAnnotationDirectory = KubernetesAnnotationPrefix + "directory"
+ KubernetesAnnotationPortForwardingAllowed = KubernetesAnnotationPrefix + "portForwardingAllowed"
+)
+
+type KubernetesRepository struct {
+ flow configuration.FlowName
+ conf *configuration.EnvironmentKubernetes
+ alternatives alternatives.Provider
+ imp imp.Imp
+
+ client bkp.Client
+
+ Logger log.Logger
+ defaultLogLevelName string
+
+ sessionIdMutex common.KeyedMutex[session.Id]
+ activeInstances sync.Map
+}
+
+func NewKubernetesRepository(_ context.Context, flow configuration.FlowName, conf *configuration.EnvironmentKubernetes, ap alternatives.Provider, i imp.Imp) (*KubernetesRepository, error) {
+ fail := func(err error) (*KubernetesRepository, error) {
+ return nil, err
+ }
+ failf := func(msg string, args ...any) (*KubernetesRepository, error) {
+ return fail(fmt.Errorf(msg, args...))
+ }
+
+ if conf == nil {
+ return failf("nil configuration")
+ }
+
+ client, err := conf.Config.GetClient(conf.Context)
+ if err != nil {
+ return fail(err)
+ }
+
+ result := KubernetesRepository{
+ flow: flow,
+ conf: conf,
+ alternatives: ap,
+ imp: i,
+ client: client,
+ }
+
+ lp := result.logger().GetProvider()
+ if la, ok := lp.(level.Aware); ok {
+ if lna, ok := lp.(level.NamesAware); ok {
+ lvl := la.GetLevel()
+ if result.defaultLogLevelName, err = lna.GetLevelNames().ToName(lvl); err != nil {
+ return failf("cannot transform to name of level %v: %w", lvl, err)
+ }
+ }
+ }
+
+ return &result, nil
+}
+
+func (this *KubernetesRepository) WillBeAccepted(ctx Context) (ok bool, err error) {
+ fail := func(err error) (bool, error) {
+ return false, err
+ }
+
+ if ok, err = this.conf.LoginAllowed.Render(ctx); err != nil {
+ return fail(fmt.Errorf("cannot evaluate if user is allowed to login or not: %w", err))
+ }
+
+ return ok, nil
+}
+
+func (this *KubernetesRepository) DoesSupportPty(Context, glssh.Pty) (bool, error) {
+ return true, nil
+}
+
+func (this *KubernetesRepository) Ensure(req Request) (Environment, error) {
+ fail := func(err error) (Environment, error) {
+ return nil, err
+ }
+ failf := func(t errors.Type, msg string, args ...any) (Environment, error) {
+ return fail(errors.Newf(t, msg, args...))
+ }
+
+ if ok, err := this.WillBeAccepted(req); err != nil {
+ return fail(err)
+ } else if !ok {
+ return fail(ErrNotAcceptable)
+ }
+
+ sess := req.Authorization().FindSession()
+ if sess == nil {
+ return failf(errors.System, "authorization without session")
+ }
+
+ return this.findOrEnsureBySession(req.Context(), sess, nil, req, true)
+}
+
+func (this *KubernetesRepository) createPodBy(req Request, sess session.Session) (*v1.Pod, error) {
+ fail := func(err error) (*v1.Pod, error) {
+ return nil, err
+ }
+ failf := func(t errors.Type, msg string, args ...any) (*v1.Pod, error) {
+ return fail(errors.Newf(t, msg, args...))
+ }
+
+ config, err := this.resolvePodConfig(req, sess)
+ if err != nil {
+ return fail(err)
+ }
+
+ clientSet, err := this.client.ClientSet()
+ if err != nil {
+ return fail(err)
+ }
+
+ client := clientSet.CoreV1().Pods(config.Namespace)
+
+ created, err := client.Create(req.Context(), config, metav1.CreateOptions{
+ FieldValidation: "Strict",
+ })
+ if err != nil {
+ var status kerrors.APIStatus
+ if errors.As(err, &status) && status.Status().Code == 404 && status.Status().Details != nil && status.Status().Details.Kind == "namespaces" {
+ if err := this.createNamespace(req.Context(), config.Namespace); err != nil {
+ return failf(errors.System, "cannot create POD: namespace does not exist and can't be created: %w", err)
+ }
+ created, err = client.Create(req.Context(), config, metav1.CreateOptions{
+ FieldValidation: "Strict",
+ })
+ }
+ }
+ if err != nil {
+ return failf(errors.System, "cannot create POD: %w", err)
+ }
+
+ watch, err := client.Watch(req.Context(), metav1.ListOptions{
+ FieldSelector: "metadata.name=" + created.Name,
+ })
+ if err != nil {
+ return failf(errors.System, "cannot watch POD %v/%v: %w", created.Namespace, created.Namespace, err)
+ }
+ defer watch.Stop()
+
+ var readyTimeout time.Duration
+ if readyTimeout, err = this.conf.ReadyTimeout.Render(req); err != nil {
+ return fail(err)
+ }
+
+ readyTimer := time.NewTimer(readyTimeout)
+ defer readyTimer.Stop()
+ for {
+ select {
+ case <-readyTimer.C:
+ // TODO! Report failed - timeout.
+ return nil, errors.System.Newf("pod %v/%v was still not ready after %v", created.Namespace, created.Name, readyTimeout)
+ case <-req.Context().Done():
+ return nil, req.Context().Err()
+ case event := <-watch.ResultChan():
+ if p, ok := event.Object.(*v1.Pod); ok {
+ switch p.Status.Phase {
+ case v1.PodPending:
+ // TODO! Report pending...
+ case v1.PodRunning:
+ // TODO! Report ready...
+ return p, nil
+ default:
+ // TODO! Report failed...
+ return nil, errors.System.Newf("pod %v/%v is unexpted state, see kubernetes logs for more details", p.Namespace, p.Name)
+ }
+ }
+ }
+ }
+}
+
+func (this *KubernetesRepository) createNamespace(ctx context.Context, namespace string) error {
+ fail := func(err error) error {
+ return errors.System.Newf("cannot create namespace %q: %w", namespace, err)
+ }
+
+ clientSet, err := this.client.ClientSet()
+ if err != nil {
+ return fail(err)
+ }
+
+ var req v1.Namespace
+ req.Name = namespace
+ if _, err := clientSet.CoreV1().Namespaces().Create(ctx, &req, metav1.CreateOptions{
+ FieldValidation: "Strict",
+ }); err != nil {
+ return fail(err)
+ }
+
+ return nil
+}
+
+func (this *KubernetesRepository) resolvePullCredentials(req Request, _ string) (string, error) {
+ fail := func(err error) (string, error) {
+ return "", errors.Config.Newf("cannot resolve image pull credentials: %w", err)
+ }
+
+ plain, err := this.conf.ImagePullCredentials.Render(req)
+ if err != nil {
+ return fail(err)
+ }
+
+ if plain == "" {
+ return "", nil
+ }
+
+ if buf, err := registry.DecodeAuthConfig(plain); err == nil && (buf.Auth != "" || buf.Username != "" || buf.Password != "") {
+ // We can take it as it is, because it is in fully valid format.
+ return plain, nil
+ }
+
+ var buf registry.AuthConfig
+ if err := json.Unmarshal([]byte(plain), &buf); err == nil && (buf.Auth != "" || buf.Username != "" || buf.Password != "") {
+ // Ok, is close to be perfect, just encode it base64 url based...
+ return base64.URLEncoding.EncodeToString([]byte(plain)), nil
+ }
+
+ // Seems to be direct auth string...
+ buf.Auth = plain
+ result, err := registry.EncodeAuthConfig(buf)
+ if err != nil {
+ return fail(err)
+ }
+ return result, nil
+}
+
+func (this *KubernetesRepository) resolvePodConfig(req Request, sess session.Session) (result *v1.Pod, err error) {
+ fail := func(err error) (*v1.Pod, error) {
+ return nil, err
+ }
+ failf := func(msg string, args ...any) (*v1.Pod, error) {
+ return fail(errors.Config.Newf(msg, args...))
+ }
+
+ result = &v1.Pod{}
+
+ if result.Name, err = this.conf.Name.Render(req); err != nil {
+ return fail(err)
+ }
+ if result.Namespace, err = this.conf.Namespace.Render(req); err != nil {
+ return fail(err)
+ }
+ if result.Namespace == "" {
+ if cn := this.client.Namespace(); cn != "" {
+ result.Namespace = cn
+ } else {
+ result.Namespace = "bifroest"
+ }
+ }
+
+ remote := req.Connection().Remote()
+ result.Labels = map[string]string{
+ KubernetesLabelFlow: this.flow.String(),
+ KubernetesLabelSessionId: sess.Id().String(),
+ }
+
+ result.Annotations = map[string]string{
+ KubernetesAnnotationCreatedRemoteUser: remote.User(),
+ KubernetesAnnotationCreatedRemoteHost: remote.Host().String(),
+ }
+ if result.Annotations[KubernetesAnnotationShellCommand], err = this.resolveEncodedShellCommand(req); err != nil {
+ return fail(err)
+ }
+ if result.Annotations[KubernetesAnnotationExecCommand], err = this.resolveEncodedExecCommand(req); err != nil {
+ return fail(err)
+ }
+ if result.Annotations[KubernetesAnnotationSftpCommand], err = this.resolveEncodedSftpCommand(req); err != nil {
+ return fail(err)
+ }
+ if result.Annotations[KubernetesAnnotationUser], err = this.conf.User.Render(req); err != nil {
+ return failf("cannot evaluate user: %w", err)
+ }
+ if result.Annotations[KubernetesAnnotationDirectory], err = this.conf.Directory.Render(req); err != nil {
+ return failf("cannot evaluate directory: %w", err)
+ }
+ if v, err := this.conf.PortForwardingAllowed.Render(req); err != nil {
+ return failf("cannot evaluate portForwardingAllowed: %w", err)
+ } else if v {
+ result.Annotations[KubernetesAnnotationPortForwardingAllowed] = "true"
+ }
+
+ if v, err := this.resolveContainerConfig(req, sess); err != nil {
+ return fail(err)
+ } else {
+ result.Spec.Containers = []v1.Container{v}
+ }
+ // TODO! Add other container configs?
+
+ if v, err := this.resolveInitContainerConfig(req, sess); err != nil {
+ return fail(err)
+ } else {
+ result.Spec.InitContainers = []v1.Container{v}
+ }
+ // TODO! Add other init container configs?
+
+ result.Spec.OS = &v1.PodOS{}
+ switch this.conf.Os {
+ case sys.OsLinux:
+ result.Spec.OS = &v1.PodOS{Name: v1.Linux}
+ case sys.OsWindows:
+ result.Spec.OS = &v1.PodOS{Name: v1.Windows}
+ default:
+ return failf("os %v is unsupported for kubernetes environments", this.conf.Os)
+ }
+
+ // TODO imagePullSecrets
+
+ if result.Spec.ServiceAccountName, err = this.conf.ServiceAccount.Render(req); err != nil {
+ return fail(err)
+ }
+
+ result.Spec.RestartPolicy = v1.RestartPolicyNever
+
+ result.Spec.Volumes = []v1.Volume{{
+ Name: "imp",
+ VolumeSource: v1.VolumeSource{
+ EmptyDir: &v1.EmptyDirVolumeSource{},
+ },
+ }}
+ // TODO! Add other volumens?
+
+ result.Spec.DNSConfig = &v1.PodDNSConfig{}
+ if result.Spec.DNSConfig.Nameservers, err = this.conf.DnsServers.Render(req); err != nil {
+ return failf("cannot evaluate dnsServer: %w", err)
+ }
+ if result.Spec.DNSConfig.Searches, err = this.conf.DnsSearch.Render(req); err != nil {
+ return failf("cannot evaluate dnsSearch: %w", err)
+ }
+
+ return result, nil
+}
+
+func (this *KubernetesRepository) resolveInitContainerConfig(req Request, _ session.Session) (result v1.Container, err error) {
+ fail := func(err error) (v1.Container, error) {
+ return v1.Container{}, err
+ }
+ failf := func(msg string, args ...any) (v1.Container, error) {
+ return fail(errors.Config.Newf(msg, args...))
+ }
+
+ result.Name = "init"
+ if result.Image, err = this.alternatives.FindOciImageFor(req.Context(), this.conf.Os, sys.ArchAmd64, alternatives.FindOciImageOpts{
+ Local: this.conf.ImageContextMode != configuration.ContextModeOnline,
+ Force: this.conf.ImageContextMode == configuration.ContextModeDebug,
+ }); err != nil {
+ return fail(err)
+ }
+
+ var targetPath string
+
+ switch this.conf.Os {
+ case sys.OsLinux:
+ targetPath = imp.DefaultInitPathUnix
+ case sys.OsWindows:
+ targetPath = imp.DefaultInitPathWindows
+ default:
+ return failf("os %v is unsupported for kubernetes environments", this.conf.Os)
+ }
+
+ result.VolumeMounts = []v1.VolumeMount{{
+ Name: "imp",
+ MountPath: targetPath,
+ }}
+
+ result.Args = strslice.StrSlice{
+ "imp-init",
+ "--targetPath=" + targetPath,
+ "--log.colorMode=always",
+ }
+ if this.defaultLogLevelName != "" {
+ result.Args = append(result.Args, `--log.level=`+this.defaultLogLevelName)
+ }
+
+ return result, nil
+}
+
+func (this *KubernetesRepository) resolveContainerConfig(req Request, sess session.Session) (result v1.Container, err error) {
+ fail := func(err error) (v1.Container, error) {
+ return v1.Container{}, err
+ }
+ failf := func(msg string, args ...any) (v1.Container, error) {
+ return fail(errors.Config.Newf(msg, args...))
+ }
+
+ result.Name = "bifroest"
+
+ if result.Image, err = this.conf.Image.Render(req); err != nil {
+ return fail(err)
+ }
+
+ switch this.conf.ImageContextMode {
+ case configuration.ContextModeDebug, configuration.ContextModeOffline:
+ result.ImagePullPolicy = v1.PullNever
+ default:
+ switch this.conf.ImagePullPolicy {
+ case configuration.PullPolicyIfAbsent:
+ result.ImagePullPolicy = v1.PullIfNotPresent
+ case configuration.PullPolicyAlways:
+ result.ImagePullPolicy = v1.PullAlways
+ case configuration.PullPolicyNever:
+ result.ImagePullPolicy = v1.PullNever
+ default:
+ return failf("image pull policy %v is not supported for kubernetes environments", this.conf.ImagePullPolicy)
+ }
+ }
+
+ result.Ports = []v1.ContainerPort{{
+ Name: "imp",
+ ContainerPort: 8683,
+ Protocol: "TCP",
+ }}
+ // TODO! Allow other ports?
+
+ result.Command = strslice.StrSlice{}
+ result.SecurityContext = &v1.SecurityContext{}
+ result.Command = []string{sys.BifroestBinaryLocation(this.conf.Os)}
+ if len(result.Command[0]) == 0 {
+ return failf("cannot resolve target path for host %v", this.conf.Os)
+ }
+ if this.conf.Os == sys.OsLinux {
+ result.SecurityContext.RunAsUser = common.P[int64](0)
+ result.SecurityContext.RunAsGroup = common.P[int64](0)
+ }
+
+ result.Args = []string{
+ `imp`,
+ `--log.colorMode=always`,
+ }
+ if this.defaultLogLevelName != "" {
+ result.Args = append(result.Args, `--log.level=`+this.defaultLogLevelName)
+ }
+
+ masterPub, err := this.imp.GetMasterPublicKey()
+ if err != nil {
+ return fail(err)
+ }
+
+ result.Env = []v1.EnvVar{{
+ Name: imp.EnvVarMasterPublicKey,
+ Value: base64.RawStdEncoding.EncodeToString(masterPub.Marshal()),
+ }, {
+ Name: session.EnvName,
+ Value: sess.Id().String(),
+ }}
+
+ result.VolumeMounts = []v1.VolumeMount{{
+ Name: "imp",
+ MountPath: result.Command[0],
+ SubPath: this.conf.Os.AppendExtToFilename("bifroest"),
+ ReadOnly: true,
+ }}
+ // TODO! Maybe add more volume mounts?
+
+ result.LivenessProbe = &v1.Probe{
+ ProbeHandler: v1.ProbeHandler{
+ TCPSocket: &v1.TCPSocketAction{
+ Port: intstr.FromInt32(imp.ServicePort),
+ },
+ },
+ PeriodSeconds: 5,
+ FailureThreshold: 1,
+ }
+
+ result.StartupProbe = &v1.Probe{
+ ProbeHandler: v1.ProbeHandler{
+ TCPSocket: &v1.TCPSocketAction{
+ Port: intstr.FromInt32(imp.ServicePort),
+ },
+ },
+ PeriodSeconds: 1,
+ FailureThreshold: 60,
+ }
+
+ if vs, err := this.conf.Capabilities.Render(req); err != nil {
+ return failf("cannot evaluate capabilities: %w", err)
+ } else {
+ result.SecurityContext.Capabilities = &v1.Capabilities{Add: *(*[]v1.Capability)(unsafe.Pointer(&vs))}
+ }
+ if v, err := this.conf.Privileged.Render(req); err != nil {
+ return failf("cannot evaluate capabilities: %w", err)
+ } else {
+ result.SecurityContext.Privileged = common.P(v)
+ }
+
+ return result, nil
+}
+
+func (this *KubernetesRepository) resolveEncodedShellCommand(req Request) (string, error) {
+ failf := func(msg string, args ...any) (string, error) {
+ return "", errors.Config.Newf(msg, args...)
+ }
+
+ v, err := this.conf.ShellCommand.Render(req)
+ if err != nil {
+ return failf("cannot evaluate shellCommand: %w", err)
+ }
+ if len(v) == 0 {
+ switch this.conf.Os {
+ case sys.OsWindows:
+ v = []string{`C:\WINDOWS\system32\cmd.exe`}
+ case sys.OsLinux:
+ v = []string{`/bin/sh`}
+ default:
+ return failf("shellCommand was not defined for kubernetes environment and default cannot be resolved for %v", this.conf.Os)
+ }
+ }
+ b, err := json.Marshal(v)
+ return string(b), err
+}
+
+func (this *KubernetesRepository) resolveEncodedExecCommand(req Request) (string, error) {
+ failf := func(msg string, args ...any) (string, error) {
+ return "", errors.Config.Newf(msg, args...)
+ }
+
+ v, err := this.conf.ExecCommand.Render(req)
+ if err != nil {
+ return failf("cannot evaluate execCommand: %w", err)
+ }
+ if len(v) == 0 {
+ switch this.conf.Os {
+ case sys.OsWindows:
+ v = []string{`C:\WINDOWS\system32\cmd.exe`, `/C`}
+ case sys.OsLinux:
+ v = []string{`/bin/sh`, `-c`}
+ default:
+ return failf("execCommand was not defined for kubernetes environment and default cannot be resolved for %v", this.conf.Os)
+ }
+ }
+ b, err := json.Marshal(v)
+ return string(b), err
+}
+
+func (this *KubernetesRepository) resolveEncodedSftpCommand(req Request) (string, error) {
+ failf := func(msg string, args ...any) (string, error) {
+ return "", errors.Config.Newf(msg, args...)
+ }
+
+ v, err := this.conf.SftpCommand.Render(req)
+ if err != nil {
+ return failf("cannot evaluate sftpCommand: %w", err)
+ }
+ if len(v) == 0 {
+ v = []string{sys.BifroestBinaryLocation(this.conf.Os), `sftp-server`}
+ if len(v[0]) == 0 {
+ return failf("sftpCommand was not defined for kubernetes environment and default cannot be resolved for %v", this.conf.Os)
+ }
+ }
+ b, err := json.Marshal(v)
+ return string(b), err
+}
+
+func (this *KubernetesRepository) FindBySession(ctx context.Context, sess session.Session, opts *FindOpts) (Environment, error) {
+ return this.findOrEnsureBySession(ctx, sess, opts, nil, false)
+}
+
+func (this *KubernetesRepository) findOrEnsureBySession(ctx context.Context, sess session.Session, opts *FindOpts, createUsing Request, retryAllowed bool) (Environment, error) {
+ fail := func(err error) (Environment, error) {
+ return nil, err
+ }
+
+ sessId := sess.Id()
+ rUnlocker := this.sessionIdMutex.RLock(sessId)
+ rUnlock := func() {
+ if rUnlocker != nil {
+ rUnlocker()
+ }
+ rUnlocker = nil
+ }
+ defer rUnlock()
+
+ ip, ok := this.activeInstances.Load(sessId)
+ if ok {
+ instance := ip.(*kubernetes)
+ instance.owners.Add(1)
+ return instance, nil
+ }
+
+ existing, err := this.findPodBySession(ctx, sess)
+ if err != nil {
+ return nil, err
+ }
+ if existing == nil && createUsing == nil {
+ return fail(ErrNoSuchEnvironment)
+ }
+ rUnlock()
+
+ defer this.sessionIdMutex.Lock(sessId)()
+
+ ip, ok = this.activeInstances.Load(sessId)
+ if ok {
+ instance := ip.(*kubernetes)
+ instance.owners.Add(1)
+ return instance, nil
+ }
+
+ if existing != nil && existing.Status.Phase != v1.PodPending && existing.Status.Phase != v1.PodRunning {
+ if opts.IsAutoCleanUpAllowed() || createUsing != nil {
+ if _, err := this.removePod(ctx, existing.Namespace, existing.Name); err != nil {
+ return fail(err)
+ }
+ }
+ if createUsing == nil {
+ return fail(ErrNoSuchEnvironment)
+ }
+ existing = nil
+ }
+
+ if existing == nil {
+ existing, err = this.createPodBy(createUsing, sess)
+ if err != nil {
+ return fail(err)
+ }
+ }
+
+ logger := this.logger().
+ With("namespace", existing.Namespace).
+ With("name", existing.Name).
+ With("sessionId", sessId)
+
+ removePodUnchecked := func() {
+ if _, err := this.removePod(ctx, existing.Namespace, existing.Name); err != nil {
+ logger.
+ WithError(err).
+ Warnf("cannot broken pod; need to be done manually")
+ }
+ }
+
+ instance, err := this.new(ctx, existing, logger)
+ if err != nil {
+ if errors.Is(err, podContainsProblemsErr) || sys.IsNotExist(err) {
+ if createUsing != nil {
+ removePodUnchecked()
+ if !retryAllowed {
+ return fail(err)
+ }
+ return this.findOrEnsureBySession(ctx, sess, opts, createUsing, false)
+ } else if opts.IsAutoCleanUpAllowed() {
+ removePodUnchecked()
+ return fail(ErrNoSuchEnvironment)
+ }
+ }
+ return fail(err)
+ }
+
+ this.activeInstances.Store(sessId, instance)
+
+ return instance, nil
+}
+
+func (this *KubernetesRepository) removePod(ctx context.Context, namespace, name string) (bool, error) {
+ fail := func(err error) (bool, error) {
+ return false, errors.System.Newf("cannot remove pod %v/%v: %w", namespace, name, err)
+ }
+
+ clientSet, err := this.client.ClientSet()
+ if err != nil {
+ return fail(err)
+ }
+ client := clientSet.CoreV1().Pods(namespace)
+
+ if err := client.Delete(ctx, name, metav1.DeleteOptions{}); errdefs.IsNotFound(err) {
+ return false, nil
+ } else if err != nil {
+ return fail(err)
+ }
+
+ return true, nil
+}
+
+func (this *KubernetesRepository) findPodBySession(ctx context.Context, sess session.Session) (*v1.Pod, error) {
+ fail := func(err error) (*v1.Pod, error) {
+ return nil, errors.System.Newf("cannot find pod by session %v: %w", sess, err)
+ }
+
+ client, err := this.podsClient()
+ if err != nil {
+ return fail(err)
+ }
+
+ candidates, err := client.List(ctx, metav1.ListOptions{
+ LabelSelector: KubernetesLabelSessionId + "=" + sess.Id().String(),
+ Limit: 1,
+ })
+ if err != nil {
+ return fail(err)
+ }
+ if len(candidates.Items) == 0 {
+ return nil, nil
+ }
+
+ return &candidates.Items[0], nil
+}
+
+func (this *KubernetesRepository) podsClient() (v2.PodInterface, error) {
+ clientSet, err := this.client.ClientSet()
+ if err != nil {
+ return nil, err
+ }
+
+ if v := this.conf.Namespace; v.IsHardCoded() {
+ if !v.IsZero() {
+ return clientSet.CoreV1().Pods(v.String()), nil
+ }
+ if cv := this.client.Namespace(); cv != "" {
+ return clientSet.CoreV1().Pods(cv), nil
+ }
+ }
+
+ // All namespaces fallback
+ return clientSet.CoreV1().Pods(""), nil
+}
+
+func (this *KubernetesRepository) Close() error {
+ return nil
+}
+
+func (this *KubernetesRepository) Cleanup(ctx context.Context, opts *CleanupOpts) error {
+ fail := func(err error) error {
+ return errors.System.Newf("cannot cleanup potential orhpan kubernetes containers: %w", err)
+ }
+
+ l := opts.GetLogger(this.logger)
+
+ client, err := this.podsClient()
+ if err != nil {
+ return fail(err)
+ }
+
+ listOpts := metav1.ListOptions{
+ LabelSelector: KubernetesLabelFlow,
+ }
+ for {
+ list, err := client.List(ctx, listOpts)
+ if err != nil {
+ return fail(err)
+ }
+
+ for _, candidate := range list.Items {
+ cl := l.With("namespace", candidate.Namespace).
+ With("name", candidate.Name)
+
+ var flow configuration.FlowName
+ if err := flow.Set(candidate.Labels[KubernetesLabelFlow]); err != nil || flow.IsZero() {
+ cl.WithError(err).
+ Warnf("pod does have an illegal %v label; this warn message will appear again until this is fixed; skipping...", KubernetesLabelFlow)
+ continue
+ }
+
+ cl = cl.With("flow", flow)
+
+ if flow.IsEqualTo(this.flow) {
+ cl.Debug("found pod that is owned by this flow environment; ignoring...")
+ continue
+ }
+
+ globalHasFlow, err := opts.HasFlowOfName(flow)
+ if err != nil {
+ return fail(err)
+ }
+
+ if globalHasFlow {
+ cl.Debug("found pod that is owned by another environment; ignoring...")
+ continue
+ }
+
+ shouldBeCleaned, err := this.conf.CleanOrphan.Render(kubernetesPodContext{&candidate})
+ if err != nil {
+ return fail(err)
+ }
+
+ if !shouldBeCleaned {
+ cl.Debug("found pod that isn't owned by anybody, but should be kept; ignoring...")
+ continue
+ }
+
+ if ok, err := this.removePod(ctx, candidate.Namespace, candidate.Name); err != nil {
+ cl.WithError(err).
+ Warn("cannot remove orphan pod; this message might continue appearing until manually fixed; skipping...")
+ continue
+ } else if ok {
+ cl.Info("orphan pod removed")
+ }
+ }
+
+ if list.Continue == "" {
+ return nil
+ }
+ listOpts.Continue = list.Continue
+ }
+}
+
+func (this *KubernetesRepository) logger() log.Logger {
+ if v := this.Logger; v != nil {
+ return v
+ }
+ return log.GetLogger("kubernetes-repository")
+}
+
+func (this *KubernetesRepository) isNoSuchImageError(err error) bool {
+ for {
+ if err == nil {
+ return false
+ }
+ if msg := err.Error(); strings.HasPrefix(msg, "No such image:") {
+ return true
+ }
+ ue, ok := err.(interface{ Unwrap() error })
+ if !ok {
+ return false
+ }
+ err = ue.Unwrap()
+ }
+}
+
+type kubernetesPodContext struct {
+ *v1.Pod
+}
+
+func (this *kubernetesPodContext) GetField(name string) (any, bool, error) {
+ switch name {
+ case "namespace":
+ return this.Namespace, true, nil
+ case "name":
+ return this.Name, true, nil
+ case "image":
+ for _, candidate := range this.Spec.Containers {
+ if candidate.Name == "bifroest" {
+ return candidate.Image, true, nil
+ }
+ }
+ return nil, true, nil
+ case "flow":
+ if this.Labels == nil {
+ return nil, true, nil
+ }
+ plain, ok := this.Labels[KubernetesLabelFlow]
+ if !ok {
+ return nil, true, nil
+ }
+ var flow configuration.FlowName
+ if err := flow.Set(plain); err != nil {
+ return nil, false, err
+ }
+ if flow.IsZero() {
+ return nil, true, nil
+ }
+ return flow, true, nil
+ default:
+ return nil, false, fmt.Errorf("unknown field %q", name)
+ }
+}
diff --git a/pkg/environment/kubernetes-usage-specific.go b/pkg/environment/kubernetes-usage-specific.go
new file mode 100644
index 0000000..feb80b9
--- /dev/null
+++ b/pkg/environment/kubernetes-usage-specific.go
@@ -0,0 +1,222 @@
+package environment
+
+import (
+ "context"
+ "io"
+ "os"
+ "slices"
+ "strings"
+ "sync"
+
+ log "github.com/echocat/slf4g"
+ glssh "github.com/gliderlabs/ssh"
+ v1 "k8s.io/api/core/v1"
+ "k8s.io/client-go/kubernetes/scheme"
+ "k8s.io/client-go/tools/remotecommand"
+
+ "github.com/engity-com/bifroest/pkg/common"
+ "github.com/engity-com/bifroest/pkg/connection"
+ "github.com/engity-com/bifroest/pkg/errors"
+ "github.com/engity-com/bifroest/pkg/imp"
+ "github.com/engity-com/bifroest/pkg/net"
+ "github.com/engity-com/bifroest/pkg/session"
+ "github.com/engity-com/bifroest/pkg/ssh"
+ "github.com/engity-com/bifroest/pkg/sys"
+)
+
+func (this *kubernetes) Banner(req Request) (io.ReadCloser, error) {
+ b, err := this.repository.conf.Banner.Render(req)
+ if err != nil {
+ return nil, err
+ }
+
+ return io.NopCloser(strings.NewReader(b)), nil
+}
+
+func (this *kubernetes) Run(t Task) (exitCode int, rErr error) {
+ fail := func(err error) (int, error) {
+ return -1, err
+ }
+ failf := func(msg string, args ...any) (int, error) {
+ return fail(errors.System.Newf(msg, args...))
+ }
+
+ auth := t.Authorization()
+ sess := auth.FindSession()
+ if sess == nil {
+ return failf("authorization without session is not supported to run kubernetes environment")
+ }
+ sshSess := t.SshSession()
+ l := t.Connection().Logger()
+
+ clientSet, err := this.repository.client.ClientSet()
+ if err != nil {
+ return fail(err)
+ }
+
+ req := clientSet.CoreV1().RESTClient().Post().
+ Resource("pods").
+ Namespace(this.namespace).
+ Name(this.name).
+ SubResource("exec")
+
+ opts := v1.PodExecOptions{
+ Container: "bifroest",
+ Stdin: true,
+ Stdout: true,
+ Stderr: true,
+ }
+ // TODO! this.user / this.directory is ignored!?
+
+ streamOpts := remotecommand.StreamOptions{
+ Stdin: sshSess,
+ Stdout: sshSess,
+ Stderr: sshSess.Stderr(),
+ }
+
+ ev := sys.EnvVars{}
+ if v, ok := os.LookupEnv("TZ"); ok {
+ ev.Set("TZ", v)
+ }
+ ev.AddAllOf(t.Authorization().EnvVars())
+ ev.Add(t.SshSession().Environ()...)
+ ev.Set(session.EnvName, sess.Id().String())
+ ev.Set(connection.EnvName, t.Connection().Id().String())
+
+ switch t.TaskType() {
+ case TaskTypeShell:
+ if v := sshSess.Command(); len(v) > 0 {
+ opts.Command = append(this.execCommand, v...)
+ } else {
+ opts.Command = slices.Clone(this.shellCommand)
+ }
+ case TaskTypeSftp:
+ opts.Command = slices.Clone(this.sftpCommand)
+ default:
+ return failf("illegal task type: %v", t.TaskType())
+ }
+ // TODO! Wrap this into the wrapper exec command
+
+ if ssh.AgentRequested(sshSess) {
+ ln, err := this.impSession.InitiateNamedPipe(t.Context(), t.Connection().Id(), "ssh-agent")
+ if err != nil {
+ return fail(err)
+ }
+ defer common.IgnoreCloseError(ln)
+ go ssh.ForwardAgentConnections(ln, l, sshSess)
+ ev.Set(ssh.AuthSockEnvName, ln.Path())
+ }
+
+ if ptyReq, winCh, isPty := sshSess.Pty(); isPty {
+ ev.Set("TERM", ptyReq.Term)
+ opts.TTY = true
+ streamOpts.Tty = true
+ streamOpts.TerminalSizeQueue = &terminalQueueSizeFromSsh{winCh}
+ }
+ // TODO! we need to set the environment variables some way
+ // opts.Env = ev.Strings()
+
+ req.VersionedParams(&opts, scheme.ParameterCodec)
+ exec, err := remotecommand.NewSPDYExecutor(this.repository.client.RestConfig(), "POST", req.URL())
+ if err != nil {
+ return fail(err)
+ }
+
+ signals := make(chan glssh.Signal, 1)
+ streamDone := make(chan error, 1)
+ var activeRoutines sync.WaitGroup
+ defer func() {
+ go func() {
+ activeRoutines.Wait()
+ defer close(signals)
+ defer close(streamDone)
+ }()
+ }()
+
+ activeRoutines.Add(1)
+ go func() {
+ cErr := exec.StreamWithContext(t.Context(), streamOpts)
+ if this.isRelevantError(cErr) {
+ streamDone <- cErr
+ } else {
+ streamDone <- nil
+ }
+ l.Trace("streaming finished")
+ }()
+
+ finish := func() (int, error) {
+ // TODO! Find a way to retrieve error code.
+ return 0, nil
+ }
+
+ sshSess.Signals(signals)
+ for {
+ select {
+ case s, ok := <-signals:
+ if ok {
+ this.signal(t.Context(), l, t.Connection(), s)
+ }
+ case <-t.Context().Done():
+ return -2, rErr
+ case err, ok := <-streamDone:
+ _ = sshSess.CloseWrite()
+ if ok && err != nil && rErr == nil {
+ return -1, err
+ }
+ if rErr == nil {
+ if ec, err := finish(); err != nil {
+ return -1, err
+ } else if ec >= 0 {
+ return ec, nil
+ }
+ }
+ }
+ }
+}
+
+type terminalQueueSizeFromSsh struct {
+ c <-chan glssh.Window
+}
+
+func (this *terminalQueueSizeFromSsh) Next() *remotecommand.TerminalSize {
+ win, ok := <-this.c
+ if !ok {
+ return nil
+ }
+ return &remotecommand.TerminalSize{
+ Width: uint16(win.Width),
+ Height: uint16(win.Height),
+ }
+}
+
+func (this *kubernetes) signal(ctx context.Context, logger log.Logger, conn connection.Connection, sshSignal glssh.Signal) {
+ var signal sys.Signal
+ if err := signal.Set(string(sshSignal)); err != nil {
+ signal = sys.SIGKILL
+ }
+
+ if err := this.impSession.Kill(ctx, conn.Id(), 0, signal); errors.Is(err, imp.ErrNoSuchProcess) {
+ // Ok.
+ } else if err != nil {
+ logger.WithError(err).
+ With("signal", signal).
+ Warn("cannot send signal to process")
+ }
+}
+
+func (this *kubernetes) IsPortForwardingAllowed(_ net.HostPort) (bool, error) {
+ return this.portForwardingAllowed, nil
+}
+
+func (this *kubernetes) NewDestinationConnection(ctx context.Context, dest net.HostPort) (io.ReadWriteCloser, error) {
+ if !this.portForwardingAllowed {
+ return nil, errors.Newf(errors.Permission, "portforwarning not allowed")
+ }
+
+ connId, err := connection.NewId()
+ if err != nil {
+ return nil, err
+ }
+
+ return this.impSession.InitiateTcpForward(ctx, connId, dest)
+}
diff --git a/pkg/environment/kubernetes.go b/pkg/environment/kubernetes.go
new file mode 100644
index 0000000..23ea4b3
--- /dev/null
+++ b/pkg/environment/kubernetes.go
@@ -0,0 +1,204 @@
+package environment
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ gonet "net"
+ "strconv"
+ "sync/atomic"
+ "syscall"
+ "time"
+
+ log "github.com/echocat/slf4g"
+ v1 "k8s.io/api/core/v1"
+
+ "github.com/engity-com/bifroest/pkg/common"
+ "github.com/engity-com/bifroest/pkg/connection"
+ "github.com/engity-com/bifroest/pkg/crypto"
+ "github.com/engity-com/bifroest/pkg/errors"
+ "github.com/engity-com/bifroest/pkg/imp"
+ "github.com/engity-com/bifroest/pkg/net"
+ "github.com/engity-com/bifroest/pkg/session"
+ "github.com/engity-com/bifroest/pkg/sys"
+)
+
+type kubernetes struct {
+ repository *KubernetesRepository
+
+ name string
+ namespace string
+ sessionId session.Id
+
+ remoteUser string
+ remoteHost net.Host
+
+ shellCommand []string
+ execCommand []string
+ sftpCommand []string
+ user string
+ directory string
+
+ portForwardingAllowed bool
+
+ impSession imp.Session
+
+ owners atomic.Int32
+}
+
+func (this *kubernetes) SessionId() session.Id {
+ return this.sessionId
+}
+
+func (this *kubernetes) PublicKey() crypto.PublicKey {
+ return nil
+}
+
+func (this *kubernetes) Dial(ctx context.Context) (gonet.Conn, error) {
+ return this.repository.client.DialPod(ctx, this.namespace, this.name, strconv.Itoa(imp.ServicePort))
+}
+
+func (this *KubernetesRepository) new(ctx context.Context, pod *v1.Pod, logger log.Logger) (*kubernetes, error) {
+ fail := func(err error) (*kubernetes, error) {
+ return nil, errors.System.Newf("cannot create environment from pod %v/%v of flow %v: %w", pod.Namespace, pod.Name, this.flow, err)
+ }
+
+ result := &kubernetes{
+ repository: this,
+ }
+ if err := result.parsePod(pod); err != nil {
+ return fail(err)
+ }
+ var err error
+ if result.impSession, err = this.imp.Open(ctx, result); err != nil {
+ return fail(err)
+ }
+
+ connId, err := connection.NewId()
+ if err != nil {
+ return fail(err)
+ }
+
+ for try := 1; try <= 200; try++ {
+ if err := result.impSession.Ping(ctx, connId); err == nil {
+ break
+ } else if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) || sys.IsNotExist(err) {
+ // try waiting...
+ } else {
+ return fail(err)
+ }
+ l := logger.With("try", try)
+ if try <= 2 {
+ l.Debug("waiting for container's imp getting ready...")
+ } else {
+ l.Info("still waiting for container's imp getting ready...")
+ }
+ time.Sleep(500 * time.Millisecond)
+ }
+
+ result.owners.Add(1)
+
+ return result, nil
+}
+
+func (this *kubernetes) Dispose(ctx context.Context) (_ bool, rErr error) {
+ fail := func(err error) (bool, error) {
+ return false, errors.Newf(errors.System, "cannot dispose environment: %w", err)
+ }
+
+ defer this.repository.sessionIdMutex.Lock(this.sessionId)()
+ defer common.KeepError(&rErr, this.closeGuarded)
+
+ ok, err := this.repository.removePod(ctx, this.namespace, this.name)
+ if err != nil {
+ return fail(err)
+ }
+
+ return ok, nil
+}
+
+func (this *kubernetes) Close() (rErr error) {
+ defer this.repository.sessionIdMutex.Lock(this.sessionId)()
+
+ return this.closeGuarded()
+}
+
+func (this *kubernetes) closeGuarded() error {
+ if this.owners.Add(-1) > 0 {
+ return nil
+ }
+ this.repository.activeInstances.Delete(this.sessionId)
+ return nil
+}
+
+func (this *kubernetes) isRelevantError(err error) bool {
+ return err != nil && !errors.Is(err, syscall.EIO) && !sys.IsClosedError(err)
+}
+
+var (
+ podContainsProblemsErr = errors.System.Newf("pod contains problems")
+)
+
+func (this *kubernetes) parsePod(pod *v1.Pod) (err error) {
+ fail := func(err error) error {
+ return fmt.Errorf("%w: %v", podContainsProblemsErr, err)
+ }
+ failf := func(msg string, args ...any) error {
+ return fail(errors.System.Newf(msg, args...))
+ }
+ decodeStrings := func(in string) (result []string, err error) {
+ err = json.Unmarshal([]byte(in), &result)
+ return result, err
+ }
+
+ this.name = pod.Name
+ this.namespace = pod.Namespace
+
+ labels := pod.Labels
+ if labels == nil {
+ pod.Labels = map[string]string{}
+ }
+ if v := labels[KubernetesLabelFlow]; v == "" {
+ return failf("missing label %s", KubernetesLabelFlow)
+ } else if v != this.repository.flow.String() {
+ return failf("expected flow: %v; bot container had: %v", this.repository.flow, v)
+ }
+ if v := labels[KubernetesLabelSessionId]; v == "" {
+ return failf("missing label %s", KubernetesLabelSessionId)
+ } else if err = this.sessionId.UnmarshalText([]byte(v)); err != nil {
+ return failf("cannot decode label %s: %w", KubernetesLabelSessionId, err)
+ }
+
+ annotations := pod.Annotations
+ if annotations == nil {
+ pod.Annotations = map[string]string{}
+ }
+ this.remoteUser = annotations[KubernetesAnnotationCreatedRemoteUser]
+ if v := annotations[KubernetesAnnotationCreatedRemoteHost]; v == "" {
+ return failf("missing annotation %s", KubernetesAnnotationCreatedRemoteHost)
+ } else if err = this.remoteHost.Set(v); err != nil {
+ return failf("cannot decode annotation %s: %w", KubernetesAnnotationCreatedRemoteHost, err)
+ }
+ if v := annotations[KubernetesAnnotationShellCommand]; v == "" {
+ return failf("missing annotation %s", KubernetesAnnotationShellCommand)
+ } else if this.shellCommand, err = decodeStrings(v); err != nil {
+ return failf("cannot decode annotation %s: %w", KubernetesAnnotationShellCommand, err)
+ }
+ if v := annotations[KubernetesAnnotationExecCommand]; v == "" {
+ return failf("missing annotation %s", KubernetesAnnotationExecCommand)
+ } else if this.execCommand, err = decodeStrings(v); err != nil {
+ return failf("cannot decode annotation %s: %w", KubernetesAnnotationExecCommand, err)
+ }
+ if v := annotations[KubernetesAnnotationSftpCommand]; v == "" {
+ this.sftpCommand = nil
+ } else if this.sftpCommand, err = decodeStrings(v); err != nil {
+ return failf("cannot decode annotation %s: %w", KubernetesAnnotationSftpCommand, err)
+ }
+
+ this.user = annotations[KubernetesAnnotationUser]
+ this.directory = annotations[KubernetesAnnotationDirectory]
+ this.portForwardingAllowed = annotations[KubernetesAnnotationPortForwardingAllowed] == "true"
+
+ return nil
+}
diff --git a/pkg/imp/imp.go b/pkg/imp/imp.go
index a18855e..d417139 100644
--- a/pkg/imp/imp.go
+++ b/pkg/imp/imp.go
@@ -3,15 +3,18 @@ package imp
import (
"context"
"io"
+ gonet "net"
"github.com/engity-com/bifroest/internal/imp/protocol"
"github.com/engity-com/bifroest/pkg/crypto"
- "github.com/engity-com/bifroest/pkg/net"
"github.com/engity-com/bifroest/pkg/session"
)
const (
EnvVarMasterPublicKey = "BIFROEST_MASTER_PUBLIC_KEY"
+
+ DefaultInitPathUnix = `/var/lib/engity/bifroest/init`
+ DefaultInitPathWindows = `C:\ProgramData\Engity\Bifroest\init`
)
var (
@@ -21,7 +24,7 @@ var (
type Ref interface {
SessionId() session.Id
PublicKey() crypto.PublicKey
- EndpointAddr() net.HostPort
+ Dial(context.Context) (gonet.Conn, error)
}
func NewImp(ctx context.Context, bifroestPrivateKey crypto.PrivateKey) (Imp, error) {
diff --git a/pkg/imp/imp_unix.go b/pkg/imp/imp_unix.go
new file mode 100644
index 0000000..3b52f65
--- /dev/null
+++ b/pkg/imp/imp_unix.go
@@ -0,0 +1,7 @@
+//go:build unix
+
+package imp
+
+const (
+ DefaultInitPath = DefaultInitPathUnix
+)
diff --git a/pkg/imp/imp_windows.go b/pkg/imp/imp_windows.go
new file mode 100644
index 0000000..753986b
--- /dev/null
+++ b/pkg/imp/imp_windows.go
@@ -0,0 +1,7 @@
+//go:build windows
+
+package imp
+
+const (
+ DefaultInitPath = DefaultInitPathWindows
+)
diff --git a/pkg/imp/roundtrip_test.go b/pkg/imp/roundtrip_test.go
index 1c56c8e..72291ed 100644
--- a/pkg/imp/roundtrip_test.go
+++ b/pkg/imp/roundtrip_test.go
@@ -466,6 +466,6 @@ func (this refImpl) PublicKey() crypto.PublicKey {
return nil
}
-func (this refImpl) EndpointAddr() net.HostPort {
- return roundtripTestImpAddress
+func (this refImpl) Dial(ctx context.Context) (gonet.Conn, error) {
+ return new(gonet.Dialer).DialContext(ctx, "tcp", roundtripTestImpAddress.String())
}
diff --git a/pkg/kubernetes/client-port-dial.go b/pkg/kubernetes/client-port-dial.go
new file mode 100644
index 0000000..d693bca
--- /dev/null
+++ b/pkg/kubernetes/client-port-dial.go
@@ -0,0 +1,216 @@
+package kubernetes
+
+import (
+ "context"
+ "fmt"
+ "io"
+ gonet "net"
+ "net/http"
+ "os"
+ "time"
+
+ v1 "k8s.io/api/core/v1"
+ kerrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/apimachinery/pkg/util/httpstream"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/portforward"
+ "k8s.io/client-go/transport/spdy"
+
+ "github.com/engity-com/bifroest/pkg/common"
+ "github.com/engity-com/bifroest/pkg/errors"
+)
+
+func (this *client) DialPod(ctx context.Context, namespace, name, port string) (gonet.Conn, error) {
+ fail := func(err error) (gonet.Conn, error) {
+ return nil, err
+ }
+
+ clientSet, err := this.ClientSet()
+ if err != nil {
+ return fail(err)
+ }
+
+ return this.dial(
+ ctx,
+ clientSet.CoreV1().RESTClient(),
+ schema.GroupVersionKind{
+ Version: "v1",
+ Kind: "pod",
+ },
+ namespace, name,
+ port,
+ )
+
+}
+
+func (this *client) dial(ctx context.Context, restClient rest.Interface, gvk schema.GroupVersionKind, namespace, name, port string) (gonet.Conn, error) {
+ fail := func(err error) (gonet.Conn, error) {
+ return nil, err
+ }
+
+ req := restClient.Post().
+ Resource(gvk.Kind + "s").
+ Namespace(namespace).
+ Name(name).
+ SubResource("portforward")
+
+ transport, upgrader, err := spdy.RoundTripperFor(this.RestConfig())
+ if err != nil {
+ return fail(err)
+ }
+
+ dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL())
+
+ success := false
+ rawConn, _, err := dialer.Dial(portforward.PortForwardProtocolV1Name)
+ if err != nil {
+ var statusErr kerrors.APIStatus
+ if errors.As(err, &statusErr) && statusErr.Status().Reason == metav1.StatusReasonNotFound {
+ return fail(os.ErrNotExist)
+ }
+ return fail(err)
+ }
+ defer common.IgnoreCloseErrorIfFalse(&success, rawConn)
+
+ headers := http.Header{}
+ headers.Set(v1.StreamType, v1.StreamTypeError)
+ headers.Set(v1.PortHeader, port)
+ headers.Set(v1.PortForwardRequestIDHeader, "1")
+
+ errorStream, err := rawConn.CreateStream(headers)
+ if err != nil {
+ return nil, errors.Network.Newf("error creating err stream: %w", err)
+ }
+ defer func() {
+ if !success {
+ rawConn.RemoveStreams(errorStream)
+ }
+ }()
+ defer common.IgnoreCloseErrorIfFalse(&success, errorStream)
+
+ headers.Set(v1.StreamType, v1.StreamTypeData)
+ dataStream, err := rawConn.CreateStream(headers)
+ if err != nil {
+ return nil, errors.Network.Newf("error creating data stream: %w", err)
+ }
+ defer func() {
+ if !success {
+ rawConn.RemoveStreams(dataStream)
+ }
+ }()
+ defer common.IgnoreCloseErrorIfFalse(&success, dataStream)
+
+ result := &httpstreamConn{
+ delegate: rawConn,
+ port: port,
+ errCh: make(chan error),
+ err: errorStream,
+ data: dataStream,
+ ownerOvk: gvk,
+ ownerNamespace: namespace,
+ ownerName: name,
+ }
+ go result.watchErr(ctx)
+
+ success = true
+ return result, nil
+}
+
+type httpstreamConn struct {
+ delegate httpstream.Connection
+ errCh chan error
+ data, err httpstream.Stream
+ port string
+ ownerOvk schema.GroupVersionKind
+ ownerNamespace string
+ ownerName string
+}
+
+func (this *httpstreamConn) watchErr(ctx context.Context) {
+ // This should only return if an err comes back.
+ bs, err := io.ReadAll(this.err)
+ if err != nil {
+ select {
+ case <-ctx.Done():
+ case this.errCh <- errors.Network.Newf("error during read: %w", err):
+ }
+ }
+ if len(bs) > 0 {
+ select {
+ case <-ctx.Done():
+ case this.errCh <- errors.Network.Newf("error during read: %s", string(bs)):
+ }
+ }
+}
+
+func (this *httpstreamConn) Read(b []byte) (n int, err error) {
+ select {
+ case err := <-this.errCh:
+ return 0, err
+ default:
+ return this.data.Read(b)
+ }
+}
+
+func (this *httpstreamConn) Write(b []byte) (n int, err error) {
+ select {
+ case err := <-this.errCh:
+ return 0, err
+ default:
+ return this.data.Write(b)
+ }
+}
+
+func (this *httpstreamConn) Close() (rErr error) {
+ defer common.KeepCloseError(&rErr, this.delegate)
+ defer this.delegate.RemoveStreams(this.data, this.err)
+ defer common.KeepCloseError(&rErr, this.err)
+ defer common.KeepCloseError(&rErr, this.data)
+
+ select {
+ case err := <-this.errCh:
+ return err
+ default:
+ return nil
+ }
+}
+
+func (this *httpstreamConn) LocalAddr() gonet.Addr {
+ return httpstreamAddr("local")
+}
+
+func (this *httpstreamConn) RemoteAddr() gonet.Addr {
+ return httpstreamAddr(fmt.Sprintf("%v/%s/%s:%s",
+ this.ownerOvk,
+ this.ownerNamespace,
+ this.ownerName,
+ this.port,
+ ))
+}
+
+func (this *httpstreamConn) SetDeadline(t time.Time) error {
+ this.delegate.SetIdleTimeout(time.Until(t))
+ return nil
+}
+
+func (this *httpstreamConn) SetReadDeadline(t time.Time) error {
+ this.delegate.SetIdleTimeout(time.Until(t))
+ return nil
+}
+
+func (this *httpstreamConn) SetWriteDeadline(t time.Time) error {
+ this.delegate.SetIdleTimeout(time.Until(t))
+ return nil
+}
+
+type httpstreamAddr string
+
+func (f httpstreamAddr) Network() string {
+ return "k8s-api"
+}
+
+func (f httpstreamAddr) String() string {
+ return string(f)
+}
diff --git a/pkg/kubernetes/client.go b/pkg/kubernetes/client.go
new file mode 100644
index 0000000..17f6bc6
--- /dev/null
+++ b/pkg/kubernetes/client.go
@@ -0,0 +1,71 @@
+package kubernetes
+
+import (
+ "context"
+ gonet "net"
+ "reflect"
+ "sync/atomic"
+
+ dynamicT "k8s.io/client-go/dynamic"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+
+ "github.com/engity-com/bifroest/pkg/errors"
+)
+
+type Client interface {
+ RestConfig() *rest.Config
+ ClientSet() (kubernetes.Interface, error)
+ DialPod(ctx context.Context, namespace, name, port string) (gonet.Conn, error)
+
+ ContextName() string
+ Namespace() string
+}
+
+type client struct {
+ restConfig *rest.Config
+ plainSource string
+ contextName string
+ namespace string
+
+ typed atomic.Pointer[kubernetes.Clientset]
+ dynamic atomic.Pointer[dynamicT.Interface]
+}
+
+func (this *client) String() string {
+ return this.plainSource
+}
+
+func (this *client) RestConfig() *rest.Config {
+ return this.restConfig
+}
+
+func (this *client) ClientSet() (kubernetes.Interface, error) {
+ for {
+ result := this.typed.Load()
+ if result != nil {
+ return result, nil
+ }
+
+ if this.restConfig == nil {
+ // TODO! We should find a way to implement this, too
+ return nil, errors.System.Newf("currently there is no support for mock of %v", reflect.TypeOf((*kubernetes.Interface)(nil)).Elem())
+ }
+
+ result, err := kubernetes.NewForConfig(this.restConfig)
+ if err != nil {
+ return nil, errors.System.Newf("cannot create new typed kubernetes client from %q: %w", this.plainSource, err)
+ }
+ if this.typed.CompareAndSwap(nil, result) {
+ return result, nil
+ }
+ }
+}
+
+func (this *client) ContextName() string {
+ return this.contextName
+}
+
+func (this *client) Namespace() string {
+ return this.namespace
+}
diff --git a/pkg/kubernetes/kubeconfig-loader.go b/pkg/kubernetes/kubeconfig-loader.go
new file mode 100644
index 0000000..e9cb866
--- /dev/null
+++ b/pkg/kubernetes/kubeconfig-loader.go
@@ -0,0 +1,33 @@
+package kubernetes
+
+import (
+ "dario.cat/mergo"
+ restclient "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+ clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
+)
+
+type kubeconfigLoader struct {
+ clientcmd.ClientConfigLoader
+ loader func() (*clientcmdapi.Config, error)
+ context string
+}
+
+func (this kubeconfigLoader) Load() (*clientcmdapi.Config, error) {
+ result := clientcmdapi.NewConfig()
+ result.CurrentContext = this.context
+
+ loaded, err := this.loader()
+ if err != nil {
+ return nil, err
+ }
+ if err := mergo.Merge(result, loaded); err != nil {
+ return nil, err
+ }
+
+ return result, nil
+}
+
+func (this kubeconfigLoader) IsDefaultConfig(*restclient.Config) bool {
+ return false
+}
diff --git a/pkg/kubernetes/kubeconfig-overwrite.go b/pkg/kubernetes/kubeconfig-overwrite.go
new file mode 100644
index 0000000..c41404c
--- /dev/null
+++ b/pkg/kubernetes/kubeconfig-overwrite.go
@@ -0,0 +1,61 @@
+package kubernetes
+
+import (
+ "net"
+ "os"
+ "os/user"
+ "path/filepath"
+
+ "k8s.io/client-go/rest"
+)
+
+type kubeconfigOverwrites struct {
+ defaultPath string
+
+ serviceHost string
+ serviceTokenFile string
+ serviceRootCaFile string
+ serviceNamespaceFile string
+}
+
+func (this kubeconfigOverwrites) resolveDefaultPath() string {
+ if v := this.defaultPath; v != "" {
+ return v
+ }
+ if u, err := user.Current(); err == nil {
+ return filepath.Join(u.HomeDir, ".kube", "config")
+ }
+ return filepath.Join(".kube", "config")
+}
+
+func (this kubeconfigOverwrites) resolveServiceHost() (string, error) {
+ if v := this.serviceHost; v != "" {
+ return v, nil
+ }
+ host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
+ if len(host) == 0 || len(port) == 0 {
+ return "", rest.ErrNotInCluster
+ }
+ return "https://" + net.JoinHostPort(host, port), nil
+}
+
+func (this kubeconfigOverwrites) resolveServiceTokenFile() string {
+ if v := this.serviceTokenFile; v != "" {
+ return v
+ }
+ return ServiceTokenFile
+}
+
+func (this kubeconfigOverwrites) resolveServiceRootCaFile() string {
+ if v := this.serviceRootCaFile; v != "" {
+ return v
+ }
+ return ServiceRootCAFile
+}
+
+func (this kubeconfigOverwrites) resolveServiceNamespaceFile() string {
+ if v := this.serviceNamespaceFile; v != "" {
+ return v
+ }
+ return ServiceNamespaceFile
+}
diff --git a/pkg/kubernetes/kubeconfig.go b/pkg/kubernetes/kubeconfig.go
new file mode 100644
index 0000000..966deb6
--- /dev/null
+++ b/pkg/kubernetes/kubeconfig.go
@@ -0,0 +1,248 @@
+package kubernetes
+
+import (
+ "fmt"
+ "os"
+
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+ clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
+ "k8s.io/client-go/transport"
+ certutil "k8s.io/client-go/util/cert"
+
+ "github.com/engity-com/bifroest/pkg/errors"
+ "github.com/engity-com/bifroest/pkg/sys"
+)
+
+const (
+ ServiceTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
+ ServiceRootCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
+ ServiceNamespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
+
+ KubeconfigInCluster = "incluster"
+ KubeconfigMock = "mock"
+ EnvVarKubeconfig = "KUBE_CONFIG"
+ DefaultContext = "default"
+)
+
+func NewKubeconfig(plain string) (Kubeconfig, error) {
+ var buf Kubeconfig
+ if err := buf.Set(plain); err != nil {
+ return Kubeconfig{}, err
+ }
+ return buf, nil
+}
+
+func MustNewKubeconfig(plain string) Kubeconfig {
+ buf, err := NewKubeconfig(plain)
+ if err != nil {
+ panic(err)
+ }
+ return buf
+}
+
+type Kubeconfig struct {
+ plain string
+ overwrites kubeconfigOverwrites
+}
+
+func (this *Kubeconfig) MarshalText() ([]byte, error) {
+ return []byte(this.plain), nil
+}
+
+func (this *Kubeconfig) UnmarshalText(text []byte) error {
+ plain := string(text)
+ switch plain {
+ case "", KubeconfigInCluster, KubeconfigMock:
+ *this = Kubeconfig{plain: plain, overwrites: this.overwrites}
+ return nil
+ default:
+ fi, err := os.Stat(plain)
+ if err != nil {
+ return errors.Config.Newf("illegal kubeconfig %q: %w", plain, err)
+ }
+ if fi.IsDir() {
+ return errors.Config.Newf("illegal kubeconfig %q: not a file", plain)
+ }
+ *this = Kubeconfig{plain: plain, overwrites: this.overwrites}
+ return nil
+ }
+}
+
+func (this *Kubeconfig) String() string {
+ v, err := this.MarshalText()
+ if err != nil {
+ return fmt.Sprintf("ERR: %v", err)
+ }
+ return string(v)
+}
+
+func (this *Kubeconfig) Validate() error {
+ return nil
+}
+
+func (this *Kubeconfig) Set(plain string) error {
+ return this.UnmarshalText([]byte(plain))
+}
+
+func (this *Kubeconfig) IsZero() bool {
+ return len(this.plain) == 0
+}
+
+func (this Kubeconfig) IsEqualTo(other any) bool {
+ if other == nil {
+ return false
+ }
+ switch v := other.(type) {
+ case Kubeconfig:
+ return this.isEqualTo(&v)
+ case *Kubeconfig:
+ return this.isEqualTo(v)
+ default:
+ return false
+ }
+}
+
+func (this Kubeconfig) isEqualTo(other *Kubeconfig) bool {
+ return this.plain == other.plain
+}
+
+func (this *Kubeconfig) GetClient(contextName string) (Client, error) {
+ return this.getClient(contextName)
+}
+
+func (this *Kubeconfig) getClient(contextName string) (*client, error) {
+ switch this.plain {
+ case "":
+ return this.loadDefaultClient(contextName)
+ case KubeconfigInCluster:
+ return this.loadInclusterClient(contextName)
+ case KubeconfigMock:
+ return this.loadMockClient(contextName)
+ default:
+ return this.loadFromFileClient(this.plain, contextName)
+ }
+}
+
+func (this *Kubeconfig) loadDefaultClient(contextName string) (*client, error) {
+ if v, ok := os.LookupEnv(EnvVarKubeconfig); ok {
+ // As path was empty, but KUBE_CONFIG is set, use it's content.
+ return this.loadDirectClient(EnvVarKubeconfig, []byte(v), contextName)
+ }
+
+ if contextName == "" || contextName == DefaultContext {
+ // Try in cluster...
+ if result, err := this.loadInclusterClient(contextName); sys.IsNotExist(err) || errors.Is(err, rest.ErrNotInCluster) {
+ // Ignore...
+ } else if err != nil {
+ return nil, err
+ } else {
+ return result, nil
+ }
+ }
+
+ path := this.overwrites.resolveDefaultPath()
+ result, err := this.loadFromFileClient(path, contextName)
+ if sys.IsNotExist(err) {
+ return nil, errors.Config.Newf("neither does the default kubeconfig %q exists nor was a specific file provided nor was the environment variable %s provided nor does this instance run inside kubernetes directly", path, EnvVarKubeconfig)
+ }
+ if err != nil {
+ return nil, err
+ }
+ return result, nil
+}
+
+func (this *Kubeconfig) loadInclusterClient(contextName string) (*client, error) {
+ if contextName != "" && contextName != DefaultContext {
+ return nil, errors.Config.Newf("kubeconfig of type %s does not support contexts; but got: %s", KubeconfigInCluster, contextName)
+ }
+
+ result := client{
+ plainSource: KubeconfigInCluster,
+ restConfig: &rest.Config{
+ TLSClientConfig: rest.TLSClientConfig{},
+ },
+ }
+
+ var err error
+
+ if result.restConfig.Host, err = this.overwrites.resolveServiceHost(); err != nil {
+ return nil, err
+ }
+
+ tokenFile := this.overwrites.resolveServiceTokenFile()
+ ts := transport.NewCachedFileTokenSource(tokenFile)
+ if _, err := ts.Token(); err != nil {
+ return nil, err
+ }
+ result.restConfig.WrapTransport = transport.TokenSourceWrapTransport(ts)
+
+ rootCaFile := this.overwrites.resolveServiceRootCaFile()
+ if _, err := certutil.NewPool(rootCaFile); err != nil {
+ return nil, errors.System.Newf("expected to load root CA config from %s, but got err: %v", rootCaFile, err)
+ }
+ result.restConfig.TLSClientConfig.CAFile = rootCaFile
+
+ namespaceFile := this.overwrites.resolveServiceNamespaceFile()
+ if nsb, err := os.ReadFile(namespaceFile); err != nil {
+ return nil, errors.System.Newf("expected to load namespace from %s, but got err: %v", namespaceFile, err)
+ } else {
+ result.namespace = string(nsb)
+ }
+
+ return &result, nil
+}
+
+func (this *Kubeconfig) loadMockClient(contextName string) (*client, error) {
+ if contextName == "" || contextName == DefaultContext {
+ contextName = "mock"
+ }
+ return &client{
+ plainSource: KubeconfigMock,
+ contextName: contextName,
+ }, nil
+}
+
+func (this *Kubeconfig) loadDirectClient(plainSource string, content []byte, contextName string) (*client, error) {
+ return this.loadClientUsing(plainSource, &kubeconfigLoader{
+ context: contextName,
+ loader: func() (*clientcmdapi.Config, error) {
+ return clientcmd.Load(content)
+ },
+ })
+}
+
+func (this *Kubeconfig) loadFromFileClient(file, contextName string) (*client, error) {
+ return this.loadClientUsing(file, &kubeconfigLoader{
+ context: contextName,
+ loader: func() (*clientcmdapi.Config, error) {
+ return clientcmd.LoadFromFile(file)
+ },
+ })
+}
+
+func (this *Kubeconfig) loadClientUsing(plainSource string, loader *kubeconfigLoader) (*client, error) {
+ loadedConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
+ loader,
+ &clientcmd.ConfigOverrides{
+ CurrentContext: loader.context,
+ },
+ )
+
+ rc, err := loadedConfig.RawConfig()
+ if err != nil {
+ return nil, err
+ }
+ if rc.CurrentContext == "" {
+ return nil, clientcmd.ErrNoContext
+ }
+ restConfig, err := loadedConfig.ClientConfig()
+ if err != nil {
+ return nil, err
+ }
+ return &client{
+ plainSource: plainSource,
+ restConfig: restConfig,
+ contextName: rc.CurrentContext,
+ }, nil
+}
diff --git a/pkg/kubernetes/kubeconfig_test.go b/pkg/kubernetes/kubeconfig_test.go
new file mode 100644
index 0000000..8d50b8f
--- /dev/null
+++ b/pkg/kubernetes/kubeconfig_test.go
@@ -0,0 +1,244 @@
+package kubernetes
+
+import (
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "k8s.io/client-go/tools/clientcmd"
+)
+
+func Test_Kubeconfig_GetClient_emptyAndNoDefault_fails(t *testing.T) {
+ defer unsetEnvVarTemporary(EnvVarKubeconfig)()
+ instance := Kubeconfig{}
+ instance.overwrites.defaultPath = "resources/does_not_exist.yml"
+
+ actual, actualErr := instance.GetClient("")
+ require.ErrorContains(t, actualErr, `neither does the default kubeconfig "resources/does_not_exist.yml" exists nor was a `)
+ require.Nil(t, actual)
+}
+
+func Test_Kubeconfig_GetClient_emptyAndTwoContexts_succeeds(t *testing.T) {
+ defer unsetEnvVarTemporary(EnvVarKubeconfig)()
+ instance := Kubeconfig{}
+ instance.overwrites.defaultPath = "resources/kubeconfig_two_contexts.yml"
+
+ actual, actualErr := instance.getClient("")
+ require.NoError(t, actualErr)
+ require.NotNil(t, actual)
+ require.Equal(t, "resources/kubeconfig_two_contexts.yml", actual.plainSource)
+ require.Equal(t, "http://127.0.0.1:8080", actual.restConfig.Host)
+ require.Equal(t, "context1", actual.contextName)
+ require.Equal(t, "", actual.namespace)
+}
+
+func Test_Kubeconfig_GetClient_emptyTwoContexts_specificContext_succeeds(t *testing.T) {
+ defer unsetEnvVarTemporary(EnvVarKubeconfig)()
+ instance := Kubeconfig{}
+ instance.overwrites.defaultPath = "resources/kubeconfig_two_contexts.yml"
+
+ actual, actualErr := instance.getClient("context2")
+ require.NoError(t, actualErr)
+ require.NotNil(t, actual)
+ require.Equal(t, "resources/kubeconfig_two_contexts.yml", actual.plainSource)
+ require.Equal(t, "http://127.0.0.2:8080", actual.restConfig.Host)
+ require.Equal(t, "context2", actual.contextName)
+ require.Equal(t, "", actual.namespace)
+}
+
+func Test_Kubeconfig_GetClient_emptyAndEnvVarSet_succeeds(t *testing.T) {
+ defer setEnvVarTemporaryToFileContent(t, EnvVarKubeconfig, "resources/kubeconfig_alternative.yml")()
+ instance := Kubeconfig{}
+ instance.overwrites.defaultPath = "resources/kubeconfig_two_contexts.yml"
+
+ actual, actualErr := instance.getClient("")
+ require.NoError(t, actualErr)
+ require.NotNil(t, actual)
+ require.Equal(t, "http://127.0.0.3:8080", actual.restConfig.Host)
+ require.Equal(t, "context3", actual.contextName)
+ require.Equal(t, "", actual.namespace)
+}
+
+func Test_Kubeconfig_GetClient_emptyAndTwoContexts_withoutCurrentContext_fails(t *testing.T) {
+ defer unsetEnvVarTemporary(EnvVarKubeconfig)()
+ instance := Kubeconfig{}
+ instance.overwrites.defaultPath = "resources/kubeconfig_without_current_context.yml"
+
+ actual, actualErr := instance.getClient("")
+ require.Equal(t, clientcmd.ErrNoContext, actualErr)
+ require.Nil(t, actual)
+}
+
+func Test_Kubeconfig_GetClient_emptyTwoContexts_specificNonExistingContext_fails(t *testing.T) {
+ defer unsetEnvVarTemporary(EnvVarKubeconfig)()
+ instance := Kubeconfig{}
+ instance.overwrites.defaultPath = "resources/kubeconfig_two_contexts.yml"
+
+ actual, actualErr := instance.getClient("wrong")
+ require.ErrorContains(t, actualErr, `context "wrong" does not exist`)
+ require.Nil(t, actual)
+}
+
+func Test_Kubeconfig_GetClient_nonExistingFile_fails(t *testing.T) {
+ defer unsetEnvVarTemporary(EnvVarKubeconfig)()
+ instance := Kubeconfig{plain: "resources/does_not_exist.yml"}
+
+ actual, actualErr := instance.getClient("wrong")
+ require.ErrorIs(t, actualErr, os.ErrNotExist)
+ require.Nil(t, actual)
+}
+
+func Test_Kubeconfig_GetClient_mock_emptyContext_succeeds(t *testing.T) {
+ defer unsetEnvVarTemporary(EnvVarKubeconfig)()
+ instance := Kubeconfig{plain: "mock"}
+
+ actual, actualErr := instance.getClient("")
+ require.NoError(t, actualErr)
+ require.NotNil(t, actual)
+ require.Equal(t, "mock", actual.plainSource)
+ require.Nil(t, actual.restConfig)
+ require.Equal(t, "mock", actual.contextName)
+ require.Equal(t, "", actual.namespace)
+}
+
+func Test_Kubeconfig_GetClient_mock_specificContext_succeeds(t *testing.T) {
+ defer unsetEnvVarTemporary(EnvVarKubeconfig)()
+ instance := Kubeconfig{plain: KubeconfigMock}
+
+ actual, actualErr := instance.getClient("foobar")
+ require.NoError(t, actualErr)
+ require.NotNil(t, actual)
+ require.Equal(t, KubeconfigMock, actual.plainSource)
+ require.Nil(t, actual.restConfig)
+ require.Equal(t, "foobar", actual.contextName)
+ require.Equal(t, "", actual.namespace)
+}
+
+func Test_Kubeconfig_GetClient_incluster_succeeds(t *testing.T) {
+ defer unsetEnvVarTemporary(EnvVarKubeconfig)()
+ defer setEnvVarTemporaryTo("KUBERNETES_SERVICE_HOST", "127.0.0.66")()
+ defer setEnvVarTemporaryTo("KUBERNETES_SERVICE_PORT", "8081")()
+ instance := Kubeconfig{plain: KubeconfigInCluster}
+ instance.overwrites.serviceTokenFile = "resources/serviceaccount_token"
+ instance.overwrites.serviceRootCaFile = "resources/serviceaccount_ca.crt"
+ instance.overwrites.serviceNamespaceFile = "resources/serviceaccount_namespace"
+
+ actual, actualErr := instance.getClient("")
+ require.NoError(t, actualErr)
+ require.NotNil(t, actual)
+ require.Equal(t, KubeconfigInCluster, actual.plainSource)
+ require.Equal(t, "https://127.0.0.66:8081", actual.restConfig.Host)
+ require.Equal(t, "", actual.contextName)
+ require.Equal(t, "aNamespace", actual.namespace)
+}
+
+func Test_Kubeconfig_GetClient_incluster_withoutServiceHost_fails(t *testing.T) {
+ defer unsetEnvVarTemporary(EnvVarKubeconfig)()
+ defer unsetEnvVarTemporary("KUBERNETES_SERVICE_HOST")()
+ defer setEnvVarTemporaryTo("KUBERNETES_SERVICE_PORT", "8081")()
+ instance := Kubeconfig{plain: KubeconfigInCluster}
+ instance.overwrites.serviceTokenFile = "resources/serviceaccount_token"
+ instance.overwrites.serviceRootCaFile = "resources/serviceaccount_ca.crt"
+ instance.overwrites.serviceNamespaceFile = "resources/serviceaccount_namespace"
+
+ actual, actualErr := instance.getClient("unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined")
+ require.ErrorContains(t, actualErr, "")
+ require.Nil(t, actual)
+}
+
+func Test_Kubeconfig_GetClient_incluster_withoutServicePort_fails(t *testing.T) {
+ defer unsetEnvVarTemporary(EnvVarKubeconfig)()
+ defer setEnvVarTemporaryTo("KUBERNETES_SERVICE_HOST", "127.0.0.66")()
+ defer unsetEnvVarTemporary("KUBERNETES_SERVICE_PORT")()
+ instance := Kubeconfig{plain: KubeconfigInCluster}
+ instance.overwrites.serviceTokenFile = "resources/serviceaccount_token"
+ instance.overwrites.serviceRootCaFile = "resources/serviceaccount_ca.crt"
+ instance.overwrites.serviceNamespaceFile = "resources/serviceaccount_namespace"
+
+ actual, actualErr := instance.getClient("unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined")
+ require.ErrorContains(t, actualErr, "")
+ require.Nil(t, actual)
+}
+
+func Test_Kubeconfig_GetClient_incluster_withoutTokenFile_fails(t *testing.T) {
+ defer unsetEnvVarTemporary(EnvVarKubeconfig)()
+ defer setEnvVarTemporaryTo("KUBERNETES_SERVICE_HOST", "127.0.0.66")()
+ defer setEnvVarTemporaryTo("KUBERNETES_SERVICE_PORT", "8081")()
+ instance := Kubeconfig{plain: KubeconfigInCluster}
+ instance.overwrites.serviceTokenFile = "resources/serviceaccount_token_non_existing"
+ instance.overwrites.serviceRootCaFile = "resources/serviceaccount_ca.crt"
+ instance.overwrites.serviceNamespaceFile = "resources/serviceaccount_namespace"
+
+ actual, actualErr := instance.getClient("")
+ require.ErrorContains(t, actualErr, `failed to read token file "resources/serviceaccount_token_non_existing"`)
+ require.Nil(t, actual)
+}
+
+func Test_Kubeconfig_GetClient_incluster_withoutRootCaFile_fails(t *testing.T) {
+ defer unsetEnvVarTemporary(EnvVarKubeconfig)()
+ defer setEnvVarTemporaryTo("KUBERNETES_SERVICE_HOST", "127.0.0.66")()
+ defer setEnvVarTemporaryTo("KUBERNETES_SERVICE_PORT", "8081")()
+ instance := Kubeconfig{plain: KubeconfigInCluster}
+ instance.overwrites.serviceTokenFile = "resources/serviceaccount_token"
+ instance.overwrites.serviceRootCaFile = "resources/serviceaccount_ca.crt_non_existing"
+ instance.overwrites.serviceNamespaceFile = "resources/serviceaccount_namespace"
+
+ actual, actualErr := instance.getClient("")
+ require.ErrorContains(t, actualErr, `expected to load root CA config from resources/serviceaccount_ca.crt_non_existing`)
+ require.Nil(t, actual)
+}
+
+func Test_Kubeconfig_GetClient_incluster_withoutNamespaceFail_fails(t *testing.T) {
+ defer unsetEnvVarTemporary(EnvVarKubeconfig)()
+ defer setEnvVarTemporaryTo("KUBERNETES_SERVICE_HOST", "127.0.0.66")()
+ defer setEnvVarTemporaryTo("KUBERNETES_SERVICE_PORT", "8081")()
+ instance := Kubeconfig{plain: KubeconfigInCluster}
+ instance.overwrites.serviceTokenFile = "resources/serviceaccount_token"
+ instance.overwrites.serviceRootCaFile = "resources/serviceaccount_ca.crt"
+ instance.overwrites.serviceNamespaceFile = "resources/serviceaccount_namespace_non_existing"
+
+ actual, actualErr := instance.getClient("")
+ require.ErrorContains(t, actualErr, `expected to load namespace from resources/serviceaccount_namespace_non_existing`)
+ require.Nil(t, actual)
+}
+
+func setEnvVarTemporaryTo(key, value string) (rollback envVarRollback) {
+ if oldValue, oldContentExists := os.LookupEnv(key); oldContentExists {
+ rollback = func() {
+ _ = os.Setenv(key, oldValue)
+ }
+ } else {
+ rollback = func() {
+ _ = os.Unsetenv(key)
+ }
+ }
+ _ = os.Setenv(key, value)
+ return
+}
+
+func unsetEnvVarTemporary(key string) (rollback envVarRollback) {
+ if oldValue, oldContentExists := os.LookupEnv(key); oldContentExists {
+ rollback = func() {
+ _ = os.Setenv(key, oldValue)
+ }
+ } else {
+ rollback = func() {
+ _ = os.Unsetenv(key)
+ }
+ }
+ _ = os.Unsetenv(key)
+ return
+}
+
+func setEnvVarTemporaryToFileContent(t testing.TB, key, filename string) (rollback envVarRollback) {
+ value, err := os.ReadFile(filename)
+ if err != nil {
+ t.Errorf("cannot set contents of %s to environment %s", filename, key)
+ t.Fail()
+ return
+ }
+
+ return setEnvVarTemporaryTo(key, string(value))
+}
+
+type envVarRollback func()
diff --git a/pkg/kubernetes/resources/kubeconfig_alternative.yml b/pkg/kubernetes/resources/kubeconfig_alternative.yml
new file mode 100644
index 0000000..407ed60
--- /dev/null
+++ b/pkg/kubernetes/resources/kubeconfig_alternative.yml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Config
+current-context: context3
+
+clusters:
+ - name: cluster3
+ cluster:
+ server: http://127.0.0.3:8080
+
+users:
+ - name: user3
+
+contexts:
+ - name: context3
+ context:
+ cluster: cluster3
+ user: user3
diff --git a/pkg/kubernetes/resources/kubeconfig_two_contexts.yml b/pkg/kubernetes/resources/kubeconfig_two_contexts.yml
new file mode 100644
index 0000000..2795b6e
--- /dev/null
+++ b/pkg/kubernetes/resources/kubeconfig_two_contexts.yml
@@ -0,0 +1,27 @@
+apiVersion: v1
+kind: Config
+current-context: context1
+
+clusters:
+ - name: cluster1
+ cluster:
+ server: http://127.0.0.1:8080
+ - name: cluster2
+ cluster:
+ server: http://127.0.0.2:8080
+
+users:
+ - name: user1
+ - name: user2
+
+contexts:
+ - name: context1
+ context:
+ cluster: cluster1
+ user: user1
+
+ - name: context2
+ context:
+ cluster: cluster2
+ user: user2
+
diff --git a/pkg/kubernetes/resources/kubeconfig_without_current_context.yml b/pkg/kubernetes/resources/kubeconfig_without_current_context.yml
new file mode 100644
index 0000000..2cd828e
--- /dev/null
+++ b/pkg/kubernetes/resources/kubeconfig_without_current_context.yml
@@ -0,0 +1,26 @@
+apiVersion: v1
+kind: Config
+
+clusters:
+ - name: cluster1
+ cluster:
+ server: http://127.0.0.1:8080
+ - name: cluster2
+ cluster:
+ server: http://127.0.0.2:8080
+
+users:
+ - name: user1
+ - name: user2
+
+contexts:
+ - name: context1
+ context:
+ cluster: cluster1
+ user: user1
+
+ - name: context2
+ context:
+ cluster: cluster2
+ user: user2
+
diff --git a/pkg/kubernetes/resources/serviceaccount_ca.crt b/pkg/kubernetes/resources/serviceaccount_ca.crt
new file mode 100644
index 0000000..baa4ca4
--- /dev/null
+++ b/pkg/kubernetes/resources/serviceaccount_ca.crt
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC0zCCAbugAwIBAgIMFT/8dNKAeUUAGreXMA0GCSqGSIb3DQEBCwUAMBUxEzAR
+BgNVBAMTCmt1YmVybmV0ZXMwHhcNMTgwNzA4MTA1MjU3WhcNMjgwNzA3MTA1MjU3
+WjAVMRMwEQYDVQQDEwprdWJlcm5ldGVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEArMMkinwNfG0o8d656/oru33Ev6sW9ThjLYhG0qS8w1VAMVZpB3tt
+Hx9SPi8nyomWmjU/KCaswqgKXdddQNPWT5E6mwxm1CLR6FWptzlDA9VA6DVzASJ6
+70bqW6heD2MgPosOmIffYNXhaDYfhpQKlNOWTuLr5E5cJtZpCZmWiag+lu4887YJ
+73Ud7O+R/7zth4hR14i9rnCOyJgyyyIFPa0uHUiJaS++DB5sdhQ9ZlHkxSxshiIp
+PVXxu9WYfBYFF/XKeaCn1fg3k9PKINvj6XJ+LV58Dhvc7EqhkL50VfUYBFunQ4Lj
++M2/0ggBO+mfHjtZwZOnvy+E+YzyeExqfwIDAQABoyMwITAOBgNVHQ8BAf8EBAMC
+AQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEARLktTOsNTZkb
+HO7klyJK2hTJYFszQFwkUMGwRwY8g0AKoNQZI8kbJCAupsQBqEjZCHOhx5Sv5ZFC
+jlL4/NN4m4J8efUqfNXy7H3EWqY2rEh1ig8clPds1yiexsen4z0V3wPdTtMxWm1x
+IJ4NiUK+Nk/kFdPTaG1DL9zz7o3+rLvV3db2e+sD3pGUI9p2gVMgiIH/G1wYRLcZ
+SJ04I/AV72c91tzB+28rYvCL2IYd5YqmfVG7Ow3MmAthE6vkTbklrJhHPXFB3HU1
+EYHH1Xj2TpzoDlMe4e1tEKnDR9t7DcRJJq1i4CKEI/SenyPKdTHDRNvdBvAvcHMV
+qOt4vnJc2w==
+-----END CERTIFICATE-----
diff --git a/pkg/kubernetes/resources/serviceaccount_namespace b/pkg/kubernetes/resources/serviceaccount_namespace
new file mode 100644
index 0000000..720c0e3
--- /dev/null
+++ b/pkg/kubernetes/resources/serviceaccount_namespace
@@ -0,0 +1 @@
+aNamespace
\ No newline at end of file
diff --git a/pkg/kubernetes/resources/serviceaccount_token b/pkg/kubernetes/resources/serviceaccount_token
new file mode 100644
index 0000000..02bd091
--- /dev/null
+++ b/pkg/kubernetes/resources/serviceaccount_token
@@ -0,0 +1 @@
+QMdBp8Hc0aC4oMgQzIlghY8IkFt916fWxhitg1S4zePqvSnpuOFbhmGl0vG5yZCK0uUCvVnj1uIhiErWY4NLOP2zIjA9pHopuLND09Op4QYhNpzJhZ9RifF3sbgZYjUN1q3P0X5pRoq2pGZ4am51maeK7W79FsZDYpYktFm5OIaza1DTcd7mLmpcYakM1sMVNvGol3WXgvyqwngAlLKDPENEx3AY3h2P0a777RnTUjuhkDZhfkOSRaIXWkXgQqlISrBTa0yHGEgPonTn83RAIGULjOIVoqYfscicb2yfj0TxT33sfp1gPR8pADVWw7yTvaN1Ke1rKX9gAJAyw5jsphLH3XMNR6hia6k8lk7n70Ys8wqOrfqnPkewGUCBLPtoNyf3cPFxfYy08cAAKqCiJnYcWah7OdNARykOhTfoU5IP6M8WWMKZ21c8DcxUOM9OXI8i3IhhoZmD02UYBTENeS8lSX1pU3KyTzK1fP959ZjTyMjKEAnJ0NB6I6mUtSMNuEv10LXavWDJuRfF3ysvtqZ3FskBF5rELg51at6gainfvJqhlEH1OKSdHE9t4a98M6fwzI7wPO5e90gyj9Smq7nvnzbsE1L6wrRepvRCQh7isWt1NzAAFW62xtMnPseyncxwSPavDU35HPjPKMEn55TpnfkGzKLnoJ2gf7rd7glBME7weGATVlSzpg3eDeuGIwZjuDVwtfPevRnHz1oTBWPDQZ1uwc1sD26vGKDyZ4oTU3OXFeV0RqzUyLRSo4NjZtzzKG5EHauaxgVDhm9W7kYyN3p0cVXSCvcwTqWDJnKYgB3vs9QwKKg44ijuYw3EbVxnWfQERbimIbsq8Hd
\ No newline at end of file
diff --git a/pkg/net/address.go b/pkg/net/address.go
index d479e9b..52feea1 100644
--- a/pkg/net/address.go
+++ b/pkg/net/address.go
@@ -8,35 +8,35 @@ import (
"strings"
)
-func NewNetAddress(plain string) (NetAddress, error) {
- var buf NetAddress
+func NewAddress(plain string) (Address, error) {
+ var buf Address
if err := buf.Set(plain); err != nil {
- return NetAddress{}, nil
+ return Address{}, nil
}
return buf, nil
}
-func MustNewNetAddress(plain string) NetAddress {
- buf, err := NewNetAddress(plain)
+func MustNewAddress(plain string) Address {
+ buf, err := NewAddress(plain)
if err != nil {
panic(err)
}
return buf
}
-type NetAddress struct {
+type Address struct {
v gonet.Addr
}
-func (this NetAddress) IsZero() bool {
+func (this Address) IsZero() bool {
return this.v == nil
}
-func (this NetAddress) MarshalText() (text []byte, err error) {
+func (this Address) MarshalText() (text []byte, err error) {
return []byte(this.String()), nil
}
-func (this NetAddress) String() string {
+func (this Address) String() string {
pv := this.v
if pv == nil {
return ""
@@ -50,7 +50,7 @@ func (this NetAddress) String() string {
}
}
-func (this NetAddress) Listen() (gonet.Listener, error) {
+func (this Address) Listen() (gonet.Listener, error) {
pv := this.v
if pv == nil {
return nil, fmt.Errorf("cannot listen to empty address")
@@ -64,7 +64,7 @@ func (this NetAddress) Listen() (gonet.Listener, error) {
}
}
-func (this *NetAddress) UnmarshalText(text []byte) error {
+func (this *Address) UnmarshalText(text []byte) error {
if len(text) == 0 {
this.v = nil
return nil
@@ -98,39 +98,39 @@ func (this *NetAddress) UnmarshalText(text []byte) error {
return nil
}
-func (this *NetAddress) Set(text string) error {
+func (this *Address) Set(text string) error {
return this.UnmarshalText([]byte(text))
}
-func (this NetAddress) Get() gonet.Addr {
+func (this Address) Get() gonet.Addr {
return this.v
}
-func (this NetAddress) IsEqualTo(other any) bool {
+func (this Address) IsEqualTo(other any) bool {
if other == nil {
return false
}
switch v := other.(type) {
- case NetAddress:
+ case Address:
return this.isEqualTo(&v)
- case *NetAddress:
+ case *Address:
return this.isEqualTo(v)
default:
return false
}
}
-func (this NetAddress) isEqualTo(other *NetAddress) bool {
+func (this Address) isEqualTo(other *Address) bool {
return reflect.DeepEqual(this.v, other.v)
}
-type NetAddresses []NetAddress
+type NetAddresses []Address
func (this *NetAddresses) Trim() error {
if this == nil {
return nil
}
- *this = slices.DeleteFunc(*this, func(e NetAddress) bool {
+ *this = slices.DeleteFunc(*this, func(e Address) bool {
return e.IsZero()
})
return nil
diff --git a/pkg/net/notify-closed_windows.go b/pkg/net/notify-closed_windows.go
index 4a058b6..065f41a 100644
--- a/pkg/net/notify-closed_windows.go
+++ b/pkg/net/notify-closed_windows.go
@@ -43,6 +43,7 @@ func notifyClosed(rc syscall.RawConn, onClosed func(), onUnexpectedEnd func(erro
}()
_, err = windows.WaitForSingleObject(eventHandle, windows.INFINITE)
+ //goland:noinspection GoTypeAssertionOnErrors
if sce, ok := err.(syscall.Errno); ok && sce == 0 {
// Ok
} else if err != nil {
@@ -65,6 +66,7 @@ func notifyClosed(rc syscall.RawConn, onClosed func(), onUnexpectedEnd func(erro
func wsaCreateEvent() (windows.Handle, error) {
ret, _, err := procWSACreateEvent.Call()
+ //goland:noinspection GoTypeAssertionOnErrors
if sce, ok := err.(syscall.Errno); ok && sce == 0 {
return windows.Handle(ret), nil
}
@@ -84,6 +86,7 @@ func wsaEventSelect(fd windows.Handle, kind uint32) (windows.Handle, error) {
return 0, err
}
_, _, err = procWSAEventSelect.Call(uintptr(fd), uintptr(event), uintptr(kind))
+ //goland:noinspection GoTypeAssertionOnErrors
if sce, ok := err.(syscall.Errno); ok && sce == 0 {
return event, nil
}
diff --git a/pkg/oci/images/builder.go b/pkg/oci/images/builder.go
new file mode 100644
index 0000000..242fb35
--- /dev/null
+++ b/pkg/oci/images/builder.go
@@ -0,0 +1,256 @@
+package images
+
+import (
+ "context"
+ "embed"
+ _ "embed"
+ "io/fs"
+ "iter"
+ "strings"
+ "time"
+
+ "github.com/google/go-containerregistry/pkg/crane"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+
+ "github.com/engity-com/bifroest/pkg/common"
+ "github.com/engity-com/bifroest/pkg/errors"
+ "github.com/engity-com/bifroest/pkg/sys"
+)
+
+const (
+ ImageMinimal = "minimal"
+ ImageScratch = "scratch"
+
+ fromMinimalLinux = "alpine"
+ fromMinimalWindows = "mcr.microsoft.com/windows/nanoserver:ltsc2022"
+)
+
+var (
+ //go:embed contrib
+ contrib embed.FS
+)
+
+func NewBuilder() (*Builder, error) {
+ return &Builder{}, nil
+}
+
+type Builder struct {
+}
+
+type BuildRequest struct {
+ From string
+ Os sys.Os
+ Arch sys.Arch
+ Time time.Time
+
+ Env sys.EnvVars
+ EntryPoint []string
+ Cmd []string
+ ExposedPorts map[string]struct{}
+ Annotations map[string]string
+
+ Contents iter.Seq2[LayerItem, error]
+ BifroestBinarySource string
+ BifroestBinarySourceFs fs.FS
+ AddDummyConfiguration bool
+ AddSkeletonStructure bool
+}
+
+func (this BuildRequest) ToOciPlatform() (*v1.Platform, error) {
+ return v1.ParsePlatform(this.Os.String() + "/" + this.Arch.Oci())
+}
+
+func (this *Builder) Build(ctx context.Context, req BuildRequest) (Image, error) {
+ fail := func(err error) (Image, error) {
+ return nil, err
+ }
+ failf := func(msg string, args ...any) (Image, error) {
+ return fail(errors.System.Newf(msg, args...))
+ }
+
+ success := false
+
+ var result image
+
+ platform, err := req.ToOciPlatform()
+ if err != nil {
+ return fail(err)
+ }
+
+ if strings.EqualFold(req.From, ImageMinimal) {
+ switch req.Os {
+ case sys.OsWindows:
+ req.From = fromMinimalWindows
+ case sys.OsLinux:
+ req.From = fromMinimalLinux
+ default:
+ return failf("unsupported operating system: %v", req.Os)
+ }
+ }
+
+ var img v1.Image
+ if strings.EqualFold(req.From, ImageScratch) {
+ img = empty.Image
+ img = mutate.MediaType(img, types.OCIManifestSchema1)
+ img = mutate.ConfigMediaType(img, types.OCIConfigJSON)
+ } else {
+ if img, err = crane.Pull(req.From,
+ crane.WithPlatform(platform),
+ crane.WithContext(ctx),
+ ); err != nil {
+ return fail(err)
+ }
+ }
+
+ cfg, err := img.ConfigFile()
+ if err != nil {
+ return fail(err)
+ }
+ cfg = cfg.DeepCopy()
+ cfg.Architecture = platform.Architecture
+ cfg.OS = platform.OS
+ cfg.OSVersion = platform.OSVersion
+ cfg.OSFeatures = platform.OSFeatures
+ cfg.Variant = platform.Variant
+ cfg.Config.Labels = make(map[string]string)
+ if v := req.ExposedPorts; v != nil {
+ cfg.Config.ExposedPorts = v
+ } else {
+ cfg.Config.ExposedPorts = make(map[string]struct{})
+ }
+ if v := req.EntryPoint; len(v) > 0 {
+ cfg.Config.Entrypoint = v
+ }
+ if v := req.Cmd; len(v) > 0 {
+ cfg.Config.Cmd = v
+ }
+ cfg.Config.Env = req.Env.Strings()
+ img, err = mutate.ConfigFile(img, cfg)
+ if err != nil {
+ return fail(err)
+ }
+
+ annotations := req.Annotations
+ if annotations == nil {
+ annotations = make(map[string]string)
+ }
+ img = mutate.Annotations(img, annotations).(v1.Image)
+
+ contents, err := this.collectBaseContents(req)
+ if err != nil {
+ return fail(err)
+ }
+
+ if v := req.Contents; v != nil {
+ contents = common.JoinSeq2[LayerItem, error](v, contents)
+ }
+
+ if contents != nil {
+ bufferedLayer, err := NewTarLayer(contents, LayerOpts{
+ Os: req.Os,
+ Id: req.Os.String() + "-" + req.Arch.String(),
+ Time: req.Time,
+ })
+ if err != nil {
+ return fail(err)
+ }
+ defer common.IgnoreCloseErrorIfFalse(&success, bufferedLayer)
+
+ if img, err = mutate.AppendLayers(img, bufferedLayer.Layer); err != nil {
+ return fail(err)
+ }
+
+ result.closers = append(result.closers, bufferedLayer)
+ }
+
+ result.Image = img
+ success = true
+ return &result, nil
+}
+
+func (this *Builder) collectBaseContents(req BuildRequest) (iter.Seq2[LayerItem, error], error) {
+ fail := func(err error) (iter.Seq2[LayerItem, error], error) {
+ return nil, err
+ }
+ failf := func(msg string, args ...any) (iter.Seq2[LayerItem, error], error) {
+ return fail(errors.System.Newf(msg, args...))
+ }
+
+ var result []LayerItem
+
+ if req.AddDummyConfiguration {
+ v := LayerItem{
+ SourceFs: contrib,
+ Mode: 0644,
+ }
+
+ switch req.Os {
+ case sys.OsWindows:
+ v.TargetFile = `C:\ProgramData\Engity\Bifroest\configuration.yaml`
+ v.SourceFile = "contrib/configuration-windows.yaml"
+ case sys.OsLinux:
+ v.TargetFile = `/etc/engity/bifroest/configuration.yaml`
+ if strings.EqualFold(req.From, ImageScratch) {
+ v.SourceFile = "contrib/configuration-unix.yaml"
+ } else {
+ v.SourceFile = "contrib/configuration-unix-extended.yaml"
+ }
+ default:
+ return failf("cannot add dummy configuration for os %v", req.Os)
+ }
+
+ result = append(result, v)
+ }
+
+ if req.AddSkeletonStructure {
+ switch req.Os {
+ case sys.OsLinux:
+ if strings.EqualFold(req.From, ImageScratch) {
+ result = append(result, LayerItem{
+ SourceFs: contrib,
+ SourceFile: "contrib/passwd",
+ TargetFile: "/etc/passwd",
+ Mode: 0644,
+ }, LayerItem{
+ SourceFs: contrib,
+ SourceFile: "contrib/group",
+ TargetFile: "/etc/group",
+ Mode: 0644,
+ }, LayerItem{
+ SourceFs: contrib,
+ SourceFile: "contrib/shadow",
+ TargetFile: "/etc/shadow",
+ Mode: 0600,
+ })
+ }
+ case sys.OsWindows:
+ // ignore
+ default:
+ return failf("cannot add dummy configuration for os %v", req.Os)
+ }
+ }
+
+ if v := req.BifroestBinarySource; v != "" {
+ item := LayerItem{
+ SourceFs: req.BifroestBinarySourceFs,
+ SourceFile: v,
+ TargetFile: sys.BifroestBinaryLocation(req.Os),
+ Mode: 0755,
+ }
+
+ if len(item.TargetFile) == 0 {
+ return failf("cannot resolve Bifröest binary for os %v", req.Os)
+ }
+
+ result = append(result, item)
+ }
+
+ if len(result) == 0 {
+ return nil, nil
+ }
+
+ return common.Seq2ErrOf[LayerItem](result...), nil
+}
diff --git a/pkg/oci/images/contrib/configuration-unix-extended.yaml b/pkg/oci/images/contrib/configuration-unix-extended.yaml
new file mode 100644
index 0000000..be1c552
--- /dev/null
+++ b/pkg/oci/images/contrib/configuration-unix-extended.yaml
@@ -0,0 +1,14 @@
+startMessage: >
+ Welcome to Engity's Bifröst!
+ This instance runs a default configuration.
+ See https://bifroest.engity.org/setup/ for more details.
+
+flows:
+ - name: local
+ authorization:
+ type: local
+ pamService: "sshd"
+
+ environment:
+ type: local
+ name: "{{.authorization.user.name}}"
diff --git a/pkg/oci/images/contrib/configuration-unix.yaml b/pkg/oci/images/contrib/configuration-unix.yaml
new file mode 100644
index 0000000..99473b6
--- /dev/null
+++ b/pkg/oci/images/contrib/configuration-unix.yaml
@@ -0,0 +1,38 @@
+startMessage: >
+ Welcome to Engity's Bifröst!
+ This instance runs a demo configuration that is NOT intended for
+ production use. Therefore, it will simply display a message
+ similar to this one and will close the connection immediately.
+ See https://bifroest.engity.org/setup/ for more details.
+ You can log in to this instance with the username "demo" and the
+ password that should be printed the first time Bifröst was
+ started on this machine, before this message.
+
+ssh:
+ banner: |+
+ Transcend with Engity's Bifröst
+ ===============================
+
+ This instance runs a demo configuration that is NOT intended for
+ production use. Please refer the following page to complete the
+ setup: https://bifroest.engity.org/setup/
+
+ You should be able to log in to this instance using the credentials
+ printed the frist time Bifröst was started on this machine.
+
+flows:
+ - name: default
+ authorization:
+ type: simple
+ entries:
+ - name: demo
+ passwordFile: "/etc/engity/bifroest/passwords/demo"
+ createPasswordFileIfAbsentOfType: plain
+ environment:
+ type: dummy
+ banner: |+
+ Yay! You have successfully logged in to Bifröst.
+
+ Now refer https://bifroest.engity.org/setup/ to continue.
+
+ Bye!
diff --git a/pkg/oci/images/contrib/configuration-windows.yaml b/pkg/oci/images/contrib/configuration-windows.yaml
new file mode 100644
index 0000000..05f87bc
--- /dev/null
+++ b/pkg/oci/images/contrib/configuration-windows.yaml
@@ -0,0 +1,38 @@
+startMessage: >
+ Welcome to Engity's Bifröst!
+ This instance runs a demo configuration that is NOT intended for
+ production use. Therefore, it will simply display a message
+ similar to this one and will close the connection immediately.
+ See https://bifroest.engity.org/setup/ for more details.
+ You can log in to this instance with the username "demo" and the
+ password that should be printed the first time Bifröst was
+ started on this machine, before this message.
+
+ssh:
+ banner: |+
+ Transcend with Engity's Bifröst
+ ===============================
+
+ This instance runs a demo configuration that is NOT intended for
+ production use. Please refer the following page to complete the
+ setup: https://bifroest.engity.org/setup/
+
+ You should be able to log in to this instance using the credentials
+ printed the frist time Bifröst was started on this machine.
+
+flows:
+ - name: default
+ authorization:
+ type: simple
+ entries:
+ - name: demo
+ passwordFile: "C:\\ProgramData\\Engity\\Bifroest\\passwords\\dummy"
+ createPasswordFileIfAbsentOfType: plain
+ environment:
+ type: dummy
+ banner: |+
+ Yay! You have successfully logged in to Bifröst.
+
+ Now refer https://bifroest.engity.org/setup/ to continue.
+
+ Bye!
diff --git a/pkg/oci/images/contrib/group b/pkg/oci/images/contrib/group
new file mode 100644
index 0000000..1dbf901
--- /dev/null
+++ b/pkg/oci/images/contrib/group
@@ -0,0 +1 @@
+root:x:0:
diff --git a/pkg/oci/images/contrib/passwd b/pkg/oci/images/contrib/passwd
new file mode 100644
index 0000000..6d9b028
--- /dev/null
+++ b/pkg/oci/images/contrib/passwd
@@ -0,0 +1 @@
+root:x:0:0:root:/:/usr/bin/bifroest
diff --git a/pkg/oci/images/contrib/shadow b/pkg/oci/images/contrib/shadow
new file mode 100644
index 0000000..cafaee5
--- /dev/null
+++ b/pkg/oci/images/contrib/shadow
@@ -0,0 +1 @@
+root:*:19683:0:99999:7:::
diff --git a/pkg/oci/images/image.go b/pkg/oci/images/image.go
new file mode 100644
index 0000000..5ee6d25
--- /dev/null
+++ b/pkg/oci/images/image.go
@@ -0,0 +1,28 @@
+package images
+
+import (
+ "io"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+
+ "github.com/engity-com/bifroest/pkg/common"
+)
+
+type Image interface {
+ v1.Image
+ io.Closer
+}
+
+type image struct {
+ v1.Image
+
+ closers []io.Closer
+}
+
+func (this *image) Close() (rErr error) {
+ for _, closer := range this.closers {
+ //goland:noinspection GoDeferInLoop
+ defer common.KeepCloseError(&rErr, closer)
+ }
+ return nil
+}
diff --git a/pkg/oci/images/layer-item.go b/pkg/oci/images/layer-item.go
new file mode 100644
index 0000000..91a8e18
--- /dev/null
+++ b/pkg/oci/images/layer-item.go
@@ -0,0 +1,13 @@
+package images
+
+import (
+ "io/fs"
+ gos "os"
+)
+
+type LayerItem struct {
+ SourceFs fs.FS
+ SourceFile string
+ TargetFile string
+ Mode gos.FileMode
+}
diff --git a/pkg/oci/images/layer.go b/pkg/oci/images/layer.go
new file mode 100644
index 0000000..915493b
--- /dev/null
+++ b/pkg/oci/images/layer.go
@@ -0,0 +1,236 @@
+package images
+
+import (
+ "archive/tar"
+ "fmt"
+ "io"
+ "io/fs"
+ "iter"
+ gos "os"
+ "path"
+ "path/filepath"
+ "slices"
+ "strings"
+ "time"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/tarball"
+ "github.com/google/uuid"
+
+ "github.com/engity-com/bifroest/pkg/common"
+ "github.com/engity-com/bifroest/pkg/errors"
+ "github.com/engity-com/bifroest/pkg/sys"
+)
+
+type LayerOpts struct {
+ Os sys.Os
+ Id string
+ Time time.Time
+}
+
+func NewTarLayer(from iter.Seq2[LayerItem, error], opts LayerOpts) (*BufferedLayer, error) {
+ fail := func(err error) (*BufferedLayer, error) {
+ return nil, errors.System.Newf("cannot create tar layer: %w", err)
+ }
+ failf := func(msg string, args ...any) (*BufferedLayer, error) {
+ return fail(errors.System.Newf(msg, args...))
+ }
+
+ dir := filepath.Join("var", "layers")
+ _ = gos.MkdirAll(dir, 0755)
+
+ if opts.Os.IsZero() {
+ return failf("no os provided")
+ }
+ if opts.Id == "" {
+ u, err := uuid.NewUUID()
+ if err != nil {
+ return fail(err)
+ }
+ opts.Id = u.String()
+ }
+ if opts.Time.IsZero() {
+ opts.Time = time.Now()
+ }
+
+ fAlreadyClosed := false
+ success := false
+ f, err := gos.CreateTemp(dir, opts.Id+"-*.tar")
+ if err != nil {
+ return fail(err)
+ }
+ defer common.IgnoreErrorIfFalse(&success, func() error { return gos.Remove(f.Name()) })
+ defer common.IgnoreCloseErrorIfFalse(&fAlreadyClosed, f)
+
+ if err := createImageArtifactLayerTar(&opts, from, f); err != nil {
+ return fail(err)
+ }
+ common.IgnoreCloseError(f)
+ fAlreadyClosed = true
+
+ result := &BufferedLayer{
+ bufferFilename: f.Name(),
+ }
+
+ if result.Layer, err = tarball.LayerFromOpener(result.open); err != nil {
+ return fail(err)
+ }
+
+ success = true
+ return result, nil
+}
+
+func createImageArtifactLayerTar(opts *LayerOpts, items iter.Seq2[LayerItem, error], target io.Writer) error {
+ fail := func(err error) error {
+ return err
+ }
+ failf := func(msg string, args ...any) error {
+ return fail(fmt.Errorf(msg, args...))
+ }
+
+ tw := tar.NewWriter(target)
+
+ var format tar.Format
+ var paxRecords map[string]string
+
+ writeHeader := func(
+ dir bool,
+ name string,
+ size int64,
+ mode int64,
+ ) error {
+ header := tar.Header{
+ Name: name,
+ Size: size,
+ Mode: mode,
+ Format: format,
+ PAXRecords: paxRecords,
+ ModTime: opts.Time,
+ }
+ if dir {
+ header.Typeflag = tar.TypeDir
+ } else {
+ header.Typeflag = tar.TypeReg
+ }
+ return tw.WriteHeader(&header)
+ }
+
+ adjustTargetFilename := func(v string) string {
+ // Paths needs to be always relative
+ if len(v) > 1 && (v[0] == '/' || v[0] == '\\') {
+ v = v[1:]
+ }
+ return v
+ }
+ var dirMode int64 = 0755
+ alreadyCreatedDirectories := map[string]struct{}{}
+
+ if opts.Os == sys.OsWindows {
+ dirMode = 0555
+ format = tar.FormatPAX
+ paxRecords = map[string]string{
+ "MSWINDOWS.rawsd": windowsUserOwnerAndGroupSID,
+ }
+
+ if err := writeHeader(true, "Files", 0, dirMode); err != nil {
+ return fail(err)
+ }
+ alreadyCreatedDirectories["Files"] = struct{}{}
+ if err := writeHeader(true, "Hives", 0, dirMode); err != nil {
+ return fail(err)
+ }
+ alreadyCreatedDirectories["Hives"] = struct{}{}
+
+ adjustTargetFilename = func(v string) string {
+ // At Windows, we need to always use /, because of the TAR format.
+ // ...and they need to start always with "Files/" instead of "C:\" or similar...
+ v = strings.ReplaceAll(v, "\\", "/")
+ if len(v) > 3 && (v[0] == 'C' || v[0] == 'c') && v[1] == ':' && v[2] == '/' {
+ v = "Files/" + v[3:]
+ }
+ return v
+ }
+ }
+
+ addItem := func(item LayerItem) (rErr error) {
+ var f fs.File
+ var err error
+ if v := item.SourceFs; v != nil {
+ f, err = v.Open(item.SourceFile)
+ } else {
+ f, err = gos.Open(item.SourceFile)
+ }
+ if err != nil {
+ return fail(err)
+ }
+ defer common.KeepCloseError(&rErr, f)
+ fi, err := f.Stat()
+ if err != nil {
+ return fail(err)
+ }
+
+ targetFile := adjustTargetFilename(item.TargetFile)
+
+ var directoriesToCreate []string
+ currentPath := path.Dir(targetFile)
+ for currentPath != "." && currentPath != "" {
+ if _, ok := alreadyCreatedDirectories[currentPath]; !ok {
+ directoriesToCreate = append(directoriesToCreate, currentPath)
+ alreadyCreatedDirectories[currentPath] = struct{}{}
+ }
+ currentPath = path.Dir(currentPath)
+ }
+ slices.Reverse(directoriesToCreate)
+ for _, dir := range directoriesToCreate {
+ if err := writeHeader(true, dir, 0, dirMode); err != nil {
+ return fail(err)
+ }
+ }
+
+ if err := writeHeader(false, targetFile, fi.Size(), int64(item.Mode)); err != nil {
+ return fail(err)
+ }
+ _, err = io.Copy(tw, f)
+
+ return err
+ }
+
+ for item, err := range items {
+ if err != nil {
+ return fail(err)
+ }
+
+ if err := addItem(item); err != nil {
+ return failf("cannot add item %q -> %q: %w", item.SourceFile, item.TargetFile, err)
+ }
+ }
+
+ if err := tw.Flush(); err != nil {
+ return fail(err)
+ }
+
+ return nil
+}
+
+type BufferedLayer struct {
+ bufferFilename string
+ Layer v1.Layer
+}
+
+func (this *BufferedLayer) open() (io.ReadCloser, error) {
+ return gos.Open(this.bufferFilename)
+}
+
+func (this *BufferedLayer) Close() error {
+ err := gos.Remove(this.bufferFilename)
+ if sys.IsNotExist(err) {
+ return nil
+ }
+ return err
+}
+
+// userOwnerAndGroupSID is a magic value needed to make the binary executable
+// in a Windows container.
+//
+// owner: BUILTIN/Users group: BUILTIN/Users ($sddlValue="O:BUG:BU")
+const windowsUserOwnerAndGroupSID = "AQAAgBQAAAAkAAAAAAAAAAAAAAABAgAAAAAABSAAAAAhAgAAAQIAAAAAAAUgAAAAIQIAAA=="
diff --git a/pkg/service/context.go b/pkg/service/context.go
index c677ecf..f05ce75 100644
--- a/pkg/service/context.go
+++ b/pkg/service/context.go
@@ -158,6 +158,8 @@ func (this *environmentContext) GetField(name string) (any, bool, error) {
return this.connection.Remote(), true, nil
case "authorization":
return this.authorization, true, nil
+ case "session":
+ return this.authorization.FindSession(), true, nil
default:
return nil, false, fmt.Errorf("unknown field %q", name)
}
diff --git a/pkg/service/service-direct-tcp-ip.go b/pkg/service/service-direct-tcp-ip.go
index 0c6fded..402dcc5 100644
--- a/pkg/service/service-direct-tcp-ip.go
+++ b/pkg/service/service-direct-tcp-ip.go
@@ -163,7 +163,8 @@ func (this *service) isAcceptableNewConnectionError(err error) bool {
var sce syscall.Errno
if errors.As(err, &sce) {
- switch sce {
+ switch //goland:noinspection GoDirectComparisonOfErrors
+ sce {
case syscall.ECONNREFUSED, syscall.ETIMEDOUT, syscall.EHOSTDOWN, syscall.ENETUNREACH:
return true
default:
diff --git a/pkg/service/service.go b/pkg/service/service.go
index 27c897c..79f78e1 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -76,7 +76,7 @@ func (this *Service) Run(ctx context.Context) (rErr error) {
lns := make([]struct {
ln gonet.Listener
- addr bnet.NetAddress
+ addr bnet.Address
}, len(this.Configuration.Ssh.Addresses))
var lnMutex sync.Mutex
closeLns := func() {
@@ -85,6 +85,7 @@ func (this *Service) Run(ctx context.Context) (rErr error) {
for _, ln := range lns {
if ln.ln != nil {
+ //goland:noinspection GoDeferInLoop
defer func(target *gonet.Listener) {
*target = nil
}(&ln.ln)
diff --git a/pkg/session/fs.go b/pkg/session/fs.go
index cb367e6..37b2cf1 100644
--- a/pkg/session/fs.go
+++ b/pkg/session/fs.go
@@ -40,6 +40,10 @@ func (this *fs) init(repository *FsRepository, flow configuration.FlowName, id I
this.info.init(this)
}
+func (this *fs) GetField(name string, ce contextEnabled) (any, bool, error) {
+ return this.info.GetField(name, ce)
+}
+
func (this *fs) Info(context.Context) (Info, error) {
return &this.info, nil
}
diff --git a/pkg/sys/binaries.go b/pkg/sys/binaries.go
new file mode 100644
index 0000000..5e9761a
--- /dev/null
+++ b/pkg/sys/binaries.go
@@ -0,0 +1,17 @@
+package sys
+
+const (
+ BifroestBinaryLocationUnix = `/usr/bin/bifroest`
+ BifroestBinaryLocationWindows = `C:\Program Files\Engity\Bifroest\bifroest.exe`
+)
+
+func BifroestBinaryLocation(os Os) string {
+ switch os {
+ case OsWindows:
+ return BifroestBinaryLocationWindows
+ case OsLinux:
+ return BifroestBinaryLocationUnix
+ default:
+ return ""
+ }
+}
diff --git a/pkg/sys/env.go b/pkg/sys/env.go
index bd1f1e6..6b82f1f 100644
--- a/pkg/sys/env.go
+++ b/pkg/sys/env.go
@@ -58,13 +58,13 @@ func (this *EnvVars) AddAllOf(in EnvVars) {
}
}
-func (this *EnvVars) Strings() []string {
- if *this == nil {
+func (this EnvVars) Strings() []string {
+ if this == nil {
return nil
}
- result := make([]string, len(*this))
+ result := make([]string, len(this))
var i int
- for k, v := range *this {
+ for k, v := range this {
result[i] = k + "=" + v
i++
}
diff --git a/pkg/sys/errors.go b/pkg/sys/errors.go
index 993a0da..284497b 100644
--- a/pkg/sys/errors.go
+++ b/pkg/sys/errors.go
@@ -8,6 +8,9 @@ import (
)
func IsNotExist(err error) bool {
+ if os.IsNotExist(err) {
+ return true
+ }
var pe *os.PathError
return errors.As(err, &pe) && os.IsNotExist(pe)
}
diff --git a/pkg/sys/errors_windows.go b/pkg/sys/errors_windows.go
index 2c53417..0393290 100644
--- a/pkg/sys/errors_windows.go
+++ b/pkg/sys/errors_windows.go
@@ -17,7 +17,8 @@ func isClosedError(err error) bool {
}
var sce = &os.SyscallError{}
if errors.As(err, &sce) && sce.Err != nil {
- switch sce.Err {
+ switch //goland:noinspection GoDirectComparisonOfErrors
+ sce.Err {
case windows.WSAECONNRESET, windows.WSAECONNABORTED:
return true
}
diff --git a/pkg/sys/os.go b/pkg/sys/os.go
index bd0cdeb..68faccd 100644
--- a/pkg/sys/os.go
+++ b/pkg/sys/os.go
@@ -35,6 +35,16 @@ func (this *Os) UnmarshalText(in []byte) error {
return nil
}
+func (this Os) Validate() error {
+ _, err := this.MarshalText()
+ return err
+}
+
+func (this Os) AppendExtToFilename(filename string) string {
+ v := osToExt[this]
+ return filename + v
+}
+
func (this *Os) Set(plain string) error {
return this.UnmarshalText([]byte(plain))
}
@@ -63,6 +73,9 @@ var (
OsLinux: "linux",
OsWindows: "windows",
}
+ osToExt = map[Os]string{
+ OsWindows: ".exe",
+ }
stringToOs = func(in map[Os]string) map[string]Os {
result := make(map[string]Os, len(in))
for k, v := range in {
diff --git a/pkg/template/duration.go b/pkg/template/duration.go
new file mode 100644
index 0000000..9e4201d
--- /dev/null
+++ b/pkg/template/duration.go
@@ -0,0 +1,132 @@
+package template
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/engity-com/bifroest/internal/text/template"
+)
+
+func NewDuration(plain string) (Duration, error) {
+ var buf Duration
+ if err := buf.Set(plain); err != nil {
+ return Duration{}, err
+ }
+ return buf, nil
+}
+
+func DurationOf(v time.Duration) Duration {
+ return Duration{
+ isHardCoded: true,
+ hardCodedValue: v,
+ plain: v.String(),
+ }
+}
+
+func MustNewDuration(plain string) Duration {
+ buf, err := NewDuration(plain)
+ if err != nil {
+ panic(err)
+ }
+ return buf
+}
+
+type Duration struct {
+ isHardCoded bool
+ hardCodedValue time.Duration
+ plain string
+ tmpl *template.Template
+}
+
+func (this Duration) Render(data any) (time.Duration, error) {
+ if this.isHardCoded {
+ return this.hardCodedValue, nil
+ }
+
+ if tmpl := this.tmpl; tmpl != nil {
+ var buf strings.Builder
+ if err := tmpl.Execute(&buf, data); err != nil {
+ return 0, err
+ }
+ plain := strings.TrimSpace(buf.String())
+ if len(plain) == 0 {
+ return 0, nil
+ }
+
+ result, err := time.ParseDuration(plain)
+ if err != nil {
+ return 0, fmt.Errorf("templated duration results in a value that cannot be parsed as duration: %q", buf.String())
+ }
+ return result, nil
+ }
+ return 0, nil
+}
+
+func (this Duration) IsHardCoded() bool {
+ return this.isHardCoded
+}
+
+func (this Duration) String() string {
+ return this.plain
+}
+
+func (this Duration) IsZero() bool {
+ return len(this.plain) == 0
+}
+
+func (this Duration) MarshalText() (text []byte, err error) {
+ return []byte(this.String()), nil
+}
+
+func (this *Duration) UnmarshalText(text []byte) error {
+ if len(text) == 0 {
+ *this = Duration{
+ isHardCoded: true,
+ hardCodedValue: 0,
+ plain: "",
+ }
+ return nil
+ }
+
+ if v, err := time.ParseDuration(string(text)); err == nil {
+ *this = DurationOf(v)
+ return nil
+ }
+
+ tmpl, err := NewTemplate("duration", string(text))
+ if err != nil {
+ return fmt.Errorf("illegal duration template: %w", err)
+ }
+ *this = Duration{
+ plain: string(text),
+ tmpl: tmpl,
+ }
+ return nil
+}
+
+func (this *Duration) Set(text string) error {
+ return this.UnmarshalText([]byte(text))
+}
+
+func (this Duration) Validate() error {
+ return nil
+}
+
+func (this Duration) IsEqualTo(other any) bool {
+ if other == nil {
+ return false
+ }
+ switch v := other.(type) {
+ case Duration:
+ return this.isEqualTo(&v)
+ case *Duration:
+ return this.isEqualTo(v)
+ default:
+ return false
+ }
+}
+
+func (this Duration) isEqualTo(other *Duration) bool {
+ return this.plain == other.plain
+}
diff --git a/pkg/template/duration_test.go b/pkg/template/duration_test.go
new file mode 100644
index 0000000..70bd407
--- /dev/null
+++ b/pkg/template/duration_test.go
@@ -0,0 +1,97 @@
+package template
+
+import (
+ "errors"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestDuration(t *testing.T) {
+ cases := []struct {
+ plain string
+ data any
+ expected time.Duration
+ expectedNewErr error
+ expectedRenderErr error
+ isHardCoded bool
+ hardCodedValue time.Duration
+ }{{
+ data: map[string]any{"foo": "666s"},
+ plain: "{{.foo}}",
+ expected: time.Second * 666,
+ }, {
+ data: map[string]any{"foo": "-11"},
+ plain: "{{.foo}}",
+ expectedRenderErr: errors.New(`templated duration results in a value that cannot be parsed as duration: "-11"`),
+ }, {
+ data: map[string]any{"foobar": "666"},
+ plain: "{{.foo}}",
+ expectedRenderErr: errors.New(`template: duration:1:2: executing "duration" at <.foo>: map has no entry for key "foo"`),
+ }, {
+ data: map[string]any{"foo": map[string]any{"bar": "abc"}},
+ plain: "{{.foo.bars}}",
+ expectedRenderErr: errors.New(`template: duration:1:6: executing "duration" at <.foo.bars>: map has no entry for key "bars"`),
+ }, {
+ data: map[string]any{"foo": map[string]any{"bar": "666"}},
+ plain: "{{get .foo `bars`}}",
+ expected: 0,
+ }, {
+ data: map[string]any{"foo": map[string]any{"bar": nil}},
+ plain: "{{.foo.bar}}",
+ expected: 0,
+ }, {
+ plain: "666m",
+ expected: 666 * time.Minute,
+ isHardCoded: true,
+ hardCodedValue: 666 * time.Minute,
+ }, {
+ plain: "",
+ expected: 0,
+ isHardCoded: true,
+ hardCodedValue: 0,
+ }, {
+ plain: "-666m",
+ expected: -666 * time.Minute,
+ isHardCoded: true,
+ hardCodedValue: -666 * time.Minute,
+ }}
+ for i, c := range cases {
+ t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
+ instance, actualErr := NewDuration(c.plain)
+ if expected := c.expectedNewErr; expected != nil {
+ if actualErr != nil {
+ if actualErr.Error() != expected.Error() {
+ t.Fatalf("expected error: %v; but got: %v", expected, actualErr)
+ }
+ } else {
+ t.Fatalf("expected error %v; but got nothing", expected)
+ }
+ } else if actualErr != nil {
+ t.Fatalf("expected no error; but got: %v", actualErr)
+ }
+
+ assert.Equal(t, c.isHardCoded, instance.isHardCoded)
+ assert.Equal(t, c.hardCodedValue, instance.hardCodedValue)
+
+ actual, actualErr := instance.Render(c.data)
+ if expected := c.expectedRenderErr; expected != nil {
+ if actualErr != nil {
+ if actualErr.Error() != expected.Error() {
+ t.Fatalf("expected error: %v; but got: %v", expected, actualErr)
+ }
+ } else {
+ t.Fatalf("expected error %v; but got nothing", expected)
+ }
+ } else if actualErr != nil {
+ t.Fatalf("expected no error; but got: %v", actualErr)
+ }
+
+ if actual != c.expected {
+ t.Fatalf("expected %v; but got: %v", c.expected, actual)
+ }
+ })
+ }
+}
diff --git a/pkg/template/int64.go b/pkg/template/int64.go
index 3a30758..14597e7 100644
--- a/pkg/template/int64.go
+++ b/pkg/template/int64.go
@@ -90,7 +90,7 @@ func (this *Int64) UnmarshalText(text []byte) error {
}
if v, err := strconv.ParseInt(string(text), 10, 64); err == nil {
- *this = Int64Of(int64(v))
+ *this = Int64Of(v)
return nil
}
diff --git a/pkg/user/common_test.go b/pkg/user/common_test.go
index b2329af..1f0d5fc 100644
--- a/pkg/user/common_test.go
+++ b/pkg/user/common_test.go
@@ -32,10 +32,6 @@ func bs(ins ...string) [][]byte {
return result
}
-func newTestFile(t testing.TB, name string) *testFile {
- return newTestDir(t).file(name)
-}
-
func newNamedTestFile(t testing.TB, fn string) *testFile {
f, err := os.OpenFile(fn, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0600)
require.NoError(t, err)