Skip to content
This repository was archived by the owner on Mar 9, 2022. It is now read-only.

Commit 1f3a73d

Browse files
authored
Merge pull request #72 from Random-Liu/add-exec-sync
Add ExecSync.
2 parents 9658159 + 9b79201 commit 1f3a73d

File tree

5 files changed

+146
-14
lines changed

5 files changed

+146
-14
lines changed

pkg/metadata/container.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ package metadata
1919
import (
2020
"encoding/json"
2121

22-
"github.com/kubernetes-incubator/cri-containerd/pkg/metadata/store"
23-
22+
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
2423
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
24+
25+
"github.com/kubernetes-incubator/cri-containerd/pkg/metadata/store"
2526
)
2627

2728
// The code is very similar with sandbox.go, but there is no template support
@@ -71,8 +72,13 @@ type ContainerMetadata struct {
7172
// In fact, this field doesn't need to be checkpointed.
7273
// TODO(random-liu): Skip this during serialization when we put object
7374
// into the store directly.
74-
// TODO(random-liu): Reset this field to false during state recoverry.
75+
// TODO(random-liu): Reset this field to false during state recovery.
7576
Removing bool
77+
// TODO(random-liu): Remove following field after switching to new containerd
78+
// client.
79+
// Not including them in unit test now because they will be removed soon.
80+
// Spec is the oci runtime spec used to run the container.
81+
Spec *runtimespec.Spec
7682
}
7783

7884
// State returns current state of the container based on the metadata.

pkg/server/container_create.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ func (c *criContainerdService) CreateContainer(ctx context.Context, r *runtime.C
160160

161161
// Update container CreatedAt.
162162
meta.CreatedAt = time.Now().UnixNano()
163+
meta.Spec = spec
163164
// Add container into container store.
164165
if err := c.containerStore.Create(meta); err != nil {
165166
return nil, fmt.Errorf("failed to add container metadata %+v into store: %v",

pkg/server/container_create_test.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -565,13 +565,6 @@ func TestCreateContainer(t *testing.T) {
565565
id := resp.GetContainerId()
566566
assert.True(t, rootExists)
567567
assert.Equal(t, getContainerRootDir(c.rootDir, id), rootPath, "root directory should be created")
568-
meta, err := c.containerStore.Get(id)
569-
assert.NoError(t, err)
570-
require.NotNil(t, meta)
571-
test.expectMeta.ID = id
572-
// TODO(random-liu): Use fake clock to test CreatedAt.
573-
test.expectMeta.CreatedAt = meta.CreatedAt
574-
assert.Equal(t, test.expectMeta, meta, "container metadata should be created")
575568

576569
// Check runtime spec
577570
containersCalls := fake.GetCalledDetails()
@@ -593,5 +586,14 @@ func TestCreateContainer(t *testing.T) {
593586
Key: id,
594587
Parent: testChainID,
595588
}, prepareOpts, "prepare request should be correct")
589+
590+
meta, err := c.containerStore.Get(id)
591+
assert.NoError(t, err)
592+
require.NotNil(t, meta)
593+
test.expectMeta.ID = id
594+
// TODO(random-liu): Use fake clock to test CreatedAt.
595+
test.expectMeta.CreatedAt = meta.CreatedAt
596+
test.expectMeta.Spec = spec
597+
assert.Equal(t, test.expectMeta, meta, "container metadata should be created")
596598
}
597599
}

pkg/server/container_execsync.go

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,136 @@ limitations under the License.
1717
package server
1818

1919
import (
20-
"errors"
20+
"bytes"
21+
"encoding/json"
22+
"fmt"
23+
"io"
24+
"io/ioutil"
2125

26+
"github.com/containerd/containerd/api/services/execution"
27+
"github.com/containerd/containerd/api/types/task"
28+
prototypes "github.com/gogo/protobuf/types"
29+
"github.com/golang/glog"
30+
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
2231
"golang.org/x/net/context"
23-
2432
runtime "k8s.io/kubernetes/pkg/kubelet/apis/cri/v1alpha1"
2533
)
2634

2735
// ExecSync executes a command in the container, and returns the stdout output.
2836
// If command exits with a non-zero exit code, an error is returned.
29-
func (c *criContainerdService) ExecSync(ctx context.Context, r *runtime.ExecSyncRequest) (*runtime.ExecSyncResponse, error) {
30-
return nil, errors.New("not implemented")
37+
func (c *criContainerdService) ExecSync(ctx context.Context, r *runtime.ExecSyncRequest) (retRes *runtime.ExecSyncResponse, retErr error) {
38+
glog.V(2).Infof("ExecSync for %q with command %+v and timeout %d (s)", r.GetContainerId(), r.GetCmd(), r.GetTimeout())
39+
defer func() {
40+
if retErr == nil {
41+
glog.V(2).Infof("ExecSync for %q returns with exit code %d", r.GetContainerId(), retRes.GetExitCode())
42+
glog.V(4).Infof("ExecSync for %q outputs - stdout: %q, stderr: %q", r.GetContainerId(),
43+
retRes.GetStdout(), retRes.GetStderr())
44+
}
45+
}()
46+
47+
// Get container config from container store.
48+
meta, err := c.containerStore.Get(r.GetContainerId())
49+
if err != nil {
50+
return nil, fmt.Errorf("an error occurred when try to find container %q: %v", r.GetContainerId(), err)
51+
}
52+
id := meta.ID
53+
54+
if meta.State() != runtime.ContainerState_CONTAINER_RUNNING {
55+
return nil, fmt.Errorf("container %q is in %s state", id, criContainerStateToString(meta.State()))
56+
}
57+
58+
// TODO(random-liu): Replace the following logic with containerd client and add unit test.
59+
// Prepare streaming pipes.
60+
execDir, err := ioutil.TempDir(getContainerRootDir(c.rootDir, id), "exec")
61+
if err != nil {
62+
return nil, fmt.Errorf("failed to create exec streaming directory: %v", err)
63+
}
64+
defer func() {
65+
if err = c.os.RemoveAll(execDir); err != nil {
66+
glog.Errorf("Failed to remove exec streaming directory %q: %v", execDir, err)
67+
}
68+
}()
69+
_, stdout, stderr := getStreamingPipes(execDir)
70+
_, stdoutPipe, stderrPipe, err := c.prepareStreamingPipes(ctx, "", stdout, stderr)
71+
if err != nil {
72+
return nil, fmt.Errorf("failed to prepare streaming pipes: %v", err)
73+
}
74+
defer stdoutPipe.Close()
75+
defer stderrPipe.Close()
76+
77+
// Start redirecting exec output.
78+
stdoutBuf, stderrBuf := new(bytes.Buffer), new(bytes.Buffer)
79+
go io.Copy(stdoutBuf, stdoutPipe) // nolint: errcheck
80+
go io.Copy(stderrBuf, stderrPipe) // nolint: errcheck
81+
82+
// Get containerd event client first, so that we won't miss any events.
83+
// TODO(random-liu): Handle this in event handler. Create an events client for
84+
// each exec introduces unnecessary overhead.
85+
cancellable, cancel := context.WithCancel(ctx)
86+
events, err := c.taskService.Events(cancellable, &execution.EventsRequest{})
87+
if err != nil {
88+
return nil, fmt.Errorf("failed to get containerd event: %v", err)
89+
}
90+
91+
spec := &meta.Spec.Process
92+
spec.Args = r.GetCmd()
93+
rawSpec, err := json.Marshal(spec)
94+
if err != nil {
95+
return nil, fmt.Errorf("failed to marshal oci spec %+v: %v", spec, err)
96+
}
97+
98+
resp, err := c.taskService.Exec(ctx, &execution.ExecRequest{
99+
ContainerID: id,
100+
Terminal: false,
101+
Stdout: stdout,
102+
Stderr: stderr,
103+
Spec: &prototypes.Any{
104+
TypeUrl: runtimespec.Version,
105+
Value: rawSpec,
106+
},
107+
})
108+
if err != nil {
109+
return nil, fmt.Errorf("failed to exec in container %q: %v", id, err)
110+
}
111+
exitCode, err := waitContainerExec(cancel, events, id, resp.Pid, r.GetTimeout())
112+
if err != nil {
113+
return nil, fmt.Errorf("failed to wait for exec in container %q to finish: %v", id, err)
114+
}
115+
116+
// TODO(random-liu): Make sure stdout/stderr are drained.
117+
return &runtime.ExecSyncResponse{
118+
Stdout: stdoutBuf.Bytes(),
119+
Stderr: stderrBuf.Bytes(),
120+
ExitCode: int32(exitCode),
121+
}, nil
122+
}
123+
124+
// waitContainerExec waits for container exec to finish and returns the exit code.
125+
func waitContainerExec(cancel context.CancelFunc, events execution.Tasks_EventsClient, id string,
126+
pid uint32, timeout int64) (uint32, error) {
127+
// TODO(random-liu): [P1] Support ExecSync timeout.
128+
// TODO(random-liu): Delete process after containerd upgrade.
129+
defer func() {
130+
// Stop events and drain the event channel. grpc-go#188
131+
cancel()
132+
for {
133+
_, err := events.Recv()
134+
if err != nil {
135+
break
136+
}
137+
}
138+
}()
139+
for {
140+
e, err := events.Recv()
141+
if err != nil {
142+
// Return non-zero exit code just in case.
143+
return unknownExitCode, err
144+
}
145+
if e.Type != task.Event_EXIT {
146+
continue
147+
}
148+
if e.ID == id && e.Pid == pid {
149+
return e.ExitStatus, nil
150+
}
151+
}
31152
}

pkg/server/helpers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ const (
5050
completeExitReason = "Completed"
5151
// errorExitReason is the exit reason when container exits with code non-zero.
5252
errorExitReason = "Error"
53+
// unknownExitCode is the exit code when exit reason is unknown.
54+
unknownExitCode = 255
5355
)
5456

5557
const (

0 commit comments

Comments
 (0)