diff --git a/go.mod b/go.mod index a2840f184..075e57868 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 3471237c1..346f04045 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/container/api.go b/pkg/container/api.go index 6a24e114e..7336f346b 100644 --- a/pkg/container/api.go +++ b/pkg/container/api.go @@ -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 +} diff --git a/pkg/container/api_test.go b/pkg/container/api_test.go index de171b292..dcb17e295 100644 --- a/pkg/container/api_test.go +++ b/pkg/container/api_test.go @@ -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, }, }