Implement airship container type

This will enable airship to run containers in privileged mode
as well and to specify commands to be executed.

Change-Id: I663eb55547bb821f26a9071c24d08166a3b3d56b
This commit is contained in:
Kostiantyn Kalynovskyi 2021-02-08 15:53:28 +00:00
parent d78cbe96a1
commit 769e164b59
4 changed files with 254 additions and 6 deletions

2
go.mod
View File

@ -6,6 +6,8 @@ require (
github.com/Azure/go-autorest/autorest v0.11.7 // indirect
github.com/Masterminds/sprig/v3 v3.2.0
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/ahmetalpbalkan/dlog v0.0.0-20170105205344-4fb5f8204f26 // indirect
github.com/ahmetb/dlog v0.0.0-20170105205344-4fb5f8204f26
github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 // indirect
github.com/containerd/containerd v1.4.1 // indirect
github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce

4
go.sum
View File

@ -66,6 +66,10 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/ahmetalpbalkan/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:pzStYMLAXM7CNQjS/Wn+zK9MUxDhSUNfVvnHsyQyjs0=
github.com/ahmetalpbalkan/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:ilK+u7u1HoqaDk0mjhh27QJB7PyWMreGffEvOCoEKiY=
github.com/ahmetb/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:3YVZUqkoev4mL+aCwVOSWV4M7pN+NURHL38Z2zq5JKA=
github.com/ahmetb/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:ymXt5bw5uSNu4jveerFxE0vNYxF8ncqbptntMaFMg3k=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=

View File

@ -15,18 +15,24 @@
package container
import (
"bytes"
"context"
"fmt"
"io"
"os"
"strings"
// TODO this small library needs to be moved to airshipctl and extended
// with splitting streams into Stderr and Stdout
"github.com/ahmetb/dlog"
"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/runfn"
kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
"sigs.k8s.io/yaml"
"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/errors"
"opendev.org/airship/airshipctl/pkg/log"
)
// ClientV1Alpha1 provides airship generic container API
@ -73,7 +79,7 @@ func (c *clientV1Alpha1) Run() error {
// set default runtime
switch c.conf.Spec.Type {
case v1alpha1.GenericContainerTypeAirship, "":
return errors.ErrNotImplemented{What: "airship generic container type"}
return c.runAirship()
case v1alpha1.GenericContainerTypeKrm:
return c.runKRM()
default:
@ -81,6 +87,104 @@ func (c *clientV1Alpha1) Run() error {
}
}
func (c *clientV1Alpha1) runAirship() error {
if c.conf.Spec.Airship.ContainerRuntime == "" {
c.conf.Spec.Airship.ContainerRuntime = ContainerDriverDocker
}
var cont Container
if c.containerFunc == nil {
c.containerFunc = NewContainer
}
cont, err := c.containerFunc(
context.Background(),
c.conf.Spec.Airship.ContainerRuntime,
c.conf.Spec.Image)
if err != nil {
return err
}
// this will split the env vars into the ones to be exported and the ones that have values
contEnv := runtimeutil.NewContainerEnvFromStringSlice(c.conf.Spec.EnvVars)
envs := []string{}
for _, key := range contEnv.VarsToExport {
envs = append(envs, strings.Join([]string{key, os.Getenv(key)}, "="))
}
for key, value := range contEnv.EnvVars {
envs = append(envs, strings.Join([]string{key, value}, "="))
}
node, err := kyaml.Parse(c.conf.Config)
if err != nil {
return err
}
decoratedInput := bytes.NewBuffer([]byte{})
pipeline := &kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: c.input}},
Outputs: []kio.Writer{kio.ByteWriter{
Writer: decoratedInput,
KeepReaderAnnotations: true,
WrappingKind: kio.ResourceListKind,
WrappingAPIVersion: kio.ResourceListAPIVersion,
FunctionConfig: node,
}},
}
err = pipeline.Execute()
if err != nil {
return err
}
log.Printf("Starting container with image: '%s', cmd: '%s'",
c.conf.Spec.Image,
c.conf.Spec.Airship.Cmd)
err = cont.RunCommand(RunCommandOptions{
Privileged: c.conf.Spec.Airship.Privileged,
Cmd: c.conf.Spec.Airship.Cmd,
Mounts: convertDockerMount(c.conf.Spec.StorageMounts),
EnvVars: envs,
Input: decoratedInput,
})
if err != nil {
return err
}
log.Debugf("Waiting for container run to finish, image: '%s', cmd: '%s'",
c.conf.Spec.Image,
c.conf.Spec.Airship.Cmd)
err = cont.WaitUntilFinished()
if err != nil {
return err
}
rOut, err := cont.GetContainerLogs(GetLogOptions{Stdout: true})
if err != nil {
return err
}
defer rOut.Close()
rErr, err := cont.GetContainerLogs(GetLogOptions{Stderr: true})
if err != nil {
return err
}
defer rOut.Close()
parsedOut := dlog.NewReader(rOut)
parsedErr := dlog.NewReader(rErr)
// write container stderr to airship log output
_, err = io.Copy(log.Writer(), parsedErr)
if err != nil {
return err
}
return writeSink(c.resultsDir, parsedOut, c.output)
}
func (c *clientV1Alpha1) runKRM() error {
mounts := convertKRMMount(c.conf.Spec.StorageMounts)
fns := &runfn.RunFns{
@ -120,6 +224,24 @@ func (c *clientV1Alpha1) runKRM() error {
return fns.Execute()
}
// writeSink output to directory on filesystem sink
func writeSink(path string, rc io.Reader, out io.Writer) error {
inputs := []kio.Reader{&kio.ByteReader{Reader: rc}}
var outputs []kio.Writer
switch {
case out == nil && path != "":
log.Debugf("writing container output to files in directory %s", path)
outputs = []kio.Writer{&kio.LocalPackageWriter{PackagePath: path}}
case out != nil:
log.Debugf("writing container output to provided writer")
outputs = []kio.Writer{&kio.ByteWriter{Writer: out}}
default:
log.Debugf("writing container output to stdout")
outputs = []kio.Writer{&kio.ByteWriter{Writer: os.Stdout}}
}
return kio.Pipeline{Inputs: inputs, Outputs: outputs}.Execute()
}
func convertKRMMount(airMounts []v1alpha1.StorageMount) (fnsMounts []runtimeutil.StorageMount) {
for _, mount := range airMounts {
fnsMounts = append(fnsMounts, runtimeutil.StorageMount{
@ -131,3 +253,18 @@ func convertKRMMount(airMounts []v1alpha1.StorageMount) (fnsMounts []runtimeutil
}
return fnsMounts
}
func convertDockerMount(airMounts []v1alpha1.StorageMount) (mounts []Mount) {
for _, mount := range airMounts {
mnt := Mount{
Type: mount.MountType,
Src: mount.Src,
Dst: mount.DstPath,
}
if !mount.ReadWriteMode {
mnt.ReadOnly = true
}
mounts = append(mounts, mnt)
}
return mounts
}

View File

@ -16,10 +16,13 @@ package container
import (
"bytes"
"context"
"io"
"io/ioutil"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -91,15 +94,117 @@ func TestGenericContainer(t *testing.T) {
expectedErr: "no such file or directory",
outputPath: "directory doesn't exist",
},
{
name: "error output directory does not exist",
outputPath: "doesn't exist",
containerAPI: &v1alpha1.GenericContainer{
Spec: v1alpha1.GenericContainerSpec{
Type: v1alpha1.GenericContainerTypeAirship,
Image: "some image",
StorageMounts: []v1alpha1.StorageMount{
{
MountType: "bind",
Src: "test",
DstPath: "/mount",
},
},
},
Config: `kind: ConfigMap`,
},
expectedErr: "no such file or directory",
execFunc: func(ctx context.Context, driver, url string) (Container, error) {
return getDockerContainerMock(mockDockerClient{
containerAttach: func() (types.HijackedResponse, error) {
conn := types.HijackedResponse{
Conn: mockConn{WData: make([]byte, len([]byte("foo: bar")))},
}
return conn, nil
},
imageList: func() ([]types.ImageSummary, error) {
return []types.ImageSummary{{ID: "imgid"}}, nil
},
imageInspectWithRaw: func() (types.ImageInspect, []byte, error) {
return types.ImageInspect{
Config: &container.Config{
Cmd: []string{"testCmd"},
},
}, nil, nil
},
}), nil
},
},
{
name: "basic success airship container",
containerAPI: &v1alpha1.GenericContainer{
Spec: v1alpha1.GenericContainerSpec{
Type: v1alpha1.GenericContainerTypeAirship,
Image: "some image",
StorageMounts: []v1alpha1.StorageMount{
{
MountType: "bind",
Src: "test",
DstPath: "/mount",
},
},
},
Config: `kind: ConfigMap`,
},
execFunc: func(ctx context.Context, driver, url string) (Container, error) {
return getDockerContainerMock(mockDockerClient{
containerAttach: func() (types.HijackedResponse, error) {
conn := types.HijackedResponse{
Conn: mockConn{WData: make([]byte, len([]byte("foo: bar")))},
}
return conn, nil
},
imageList: func() ([]types.ImageSummary, error) {
return []types.ImageSummary{{ID: "imgid"}}, nil
},
imageInspectWithRaw: func() (types.ImageInspect, []byte, error) {
return types.ImageInspect{
Config: &container.Config{
Cmd: []string{"testCmd"},
},
}, nil, nil
},
}), nil
},
},
{
name: "basic success airship success written to provided output Writer",
containerAPI: &v1alpha1.GenericContainer{
Spec: v1alpha1.GenericContainerSpec{
Type: v1alpha1.GenericContainerTypeAirship,
Type: v1alpha1.GenericContainerTypeAirship,
Image: "some image",
StorageMounts: []v1alpha1.StorageMount{
{
MountType: "bind",
Src: "test",
DstPath: "/mount",
},
},
},
Config: `kind: ConfigMap`,
},
output: ioutil.Discard,
expectedErr: "airship generic container type",
execFunc: func(ctx context.Context, driver, url string) (Container, error) {
return getDockerContainerMock(mockDockerClient{
containerAttach: func() (types.HijackedResponse, error) {
conn := types.HijackedResponse{
Conn: mockConn{WData: make([]byte, len([]byte("foo: bar")))},
}
return conn, nil
},
imageList: func() ([]types.ImageSummary, error) {
return []types.ImageSummary{{ID: "imgid"}}, nil
},
imageInspectWithRaw: func() (types.ImageInspect, []byte, error) {
return types.ImageInspect{
Config: &container.Config{},
}, nil, nil
},
}), nil
},
output: ioutil.Discard,
},
}