Move cri server packages under pkg/cri

Organizes the cri related server packages under pkg/cri

Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
Derek McGowan
2020-10-07 13:09:37 -07:00
parent f44b072781
commit b22b627300
130 changed files with 109 additions and 111 deletions

View File

@@ -0,0 +1,177 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package container
import (
"sync"
"github.com/containerd/containerd"
"github.com/containerd/containerd/pkg/cri/store/label"
"github.com/docker/docker/pkg/truncindex"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
cio "github.com/containerd/containerd/pkg/cri/io"
"github.com/containerd/containerd/pkg/cri/store"
)
// Container contains all resources associated with the container. All methods to
// mutate the internal state are thread-safe.
type Container struct {
// Metadata is the metadata of the container, it is **immutable** after created.
Metadata
// Status stores the status of the container.
Status StatusStorage
// Container is the containerd container client.
Container containerd.Container
// Container IO.
// IO could only be nil when the container is in unknown state.
IO *cio.ContainerIO
// StopCh is used to propagate the stop information of the container.
*store.StopCh
}
// Opts sets specific information to newly created Container.
type Opts func(*Container) error
// WithContainer adds the containerd Container to the internal data store.
func WithContainer(cntr containerd.Container) Opts {
return func(c *Container) error {
c.Container = cntr
return nil
}
}
// WithContainerIO adds IO into the container.
func WithContainerIO(io *cio.ContainerIO) Opts {
return func(c *Container) error {
c.IO = io
return nil
}
}
// WithStatus adds status to the container.
func WithStatus(status Status, root string) Opts {
return func(c *Container) error {
s, err := StoreStatus(root, c.ID, status)
if err != nil {
return err
}
c.Status = s
if s.Get().State() == runtime.ContainerState_CONTAINER_EXITED {
c.Stop()
}
return nil
}
}
// NewContainer creates an internally used container type.
func NewContainer(metadata Metadata, opts ...Opts) (Container, error) {
c := Container{
Metadata: metadata,
StopCh: store.NewStopCh(),
}
for _, o := range opts {
if err := o(&c); err != nil {
return Container{}, err
}
}
return c, nil
}
// Delete deletes checkpoint for the container.
func (c *Container) Delete() error {
return c.Status.Delete()
}
// Store stores all Containers.
type Store struct {
lock sync.RWMutex
containers map[string]Container
idIndex *truncindex.TruncIndex
labels *label.Store
}
// NewStore creates a container store.
func NewStore(labels *label.Store) *Store {
return &Store{
containers: make(map[string]Container),
idIndex: truncindex.NewTruncIndex([]string{}),
labels: labels,
}
}
// Add a container into the store. Returns store.ErrAlreadyExist if the
// container already exists.
func (s *Store) Add(c Container) error {
s.lock.Lock()
defer s.lock.Unlock()
if _, ok := s.containers[c.ID]; ok {
return store.ErrAlreadyExist
}
if err := s.labels.Reserve(c.ProcessLabel); err != nil {
return err
}
if err := s.idIndex.Add(c.ID); err != nil {
return err
}
s.containers[c.ID] = c
return nil
}
// Get returns the container with specified id. Returns store.ErrNotExist
// if the container doesn't exist.
func (s *Store) Get(id string) (Container, error) {
s.lock.RLock()
defer s.lock.RUnlock()
id, err := s.idIndex.Get(id)
if err != nil {
if err == truncindex.ErrNotExist {
err = store.ErrNotExist
}
return Container{}, err
}
if c, ok := s.containers[id]; ok {
return c, nil
}
return Container{}, store.ErrNotExist
}
// List lists all containers.
func (s *Store) List() []Container {
s.lock.RLock()
defer s.lock.RUnlock()
var containers []Container
for _, c := range s.containers {
containers = append(containers, c)
}
return containers
}
// Delete deletes the container from store with specified id.
func (s *Store) Delete(id string) {
s.lock.Lock()
defer s.lock.Unlock()
id, err := s.idIndex.Get(id)
if err != nil {
// Note: The idIndex.Delete and delete doesn't handle truncated index.
// So we need to return if there are error.
return
}
s.labels.Release(s.containers[id].ProcessLabel)
s.idIndex.Delete(id) // nolint: errcheck
delete(s.containers, id)
}

View File

@@ -0,0 +1,247 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package container
import (
"strings"
"testing"
"time"
"github.com/containerd/containerd/pkg/cri/store/label"
"github.com/opencontainers/selinux/go-selinux"
assertlib "github.com/stretchr/testify/assert"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
cio "github.com/containerd/containerd/pkg/cri/io"
"github.com/containerd/containerd/pkg/cri/store"
)
func TestContainerStore(t *testing.T) {
metadatas := map[string]Metadata{
"1": {
ID: "1",
Name: "Container-1",
SandboxID: "Sandbox-1",
Config: &runtime.ContainerConfig{
Metadata: &runtime.ContainerMetadata{
Name: "TestPod-1",
Attempt: 1,
},
},
ImageRef: "TestImage-1",
StopSignal: "SIGTERM",
LogPath: "/test/log/path/1",
ProcessLabel: "junk:junk:junk:c1,c2",
},
"2abcd": {
ID: "2abcd",
Name: "Container-2abcd",
SandboxID: "Sandbox-2abcd",
Config: &runtime.ContainerConfig{
Metadata: &runtime.ContainerMetadata{
Name: "TestPod-2abcd",
Attempt: 2,
},
},
StopSignal: "SIGTERM",
ImageRef: "TestImage-2",
LogPath: "/test/log/path/2",
ProcessLabel: "junk:junk:junk:c1,c2",
},
"4a333": {
ID: "4a333",
Name: "Container-4a333",
SandboxID: "Sandbox-4a333",
Config: &runtime.ContainerConfig{
Metadata: &runtime.ContainerMetadata{
Name: "TestPod-4a333",
Attempt: 3,
},
},
StopSignal: "SIGTERM",
ImageRef: "TestImage-3",
LogPath: "/test/log/path/3",
ProcessLabel: "junk:junk:junk:c1,c3",
},
"4abcd": {
ID: "4abcd",
Name: "Container-4abcd",
SandboxID: "Sandbox-4abcd",
Config: &runtime.ContainerConfig{
Metadata: &runtime.ContainerMetadata{
Name: "TestPod-4abcd",
Attempt: 1,
},
},
StopSignal: "SIGTERM",
ImageRef: "TestImage-4abcd",
ProcessLabel: "junk:junk:junk:c1,c4",
},
}
statuses := map[string]Status{
"1": {
Pid: 1,
CreatedAt: time.Now().UnixNano(),
StartedAt: time.Now().UnixNano(),
FinishedAt: time.Now().UnixNano(),
ExitCode: 1,
Reason: "TestReason-1",
Message: "TestMessage-1",
},
"2abcd": {
Pid: 2,
CreatedAt: time.Now().UnixNano(),
StartedAt: time.Now().UnixNano(),
FinishedAt: time.Now().UnixNano(),
ExitCode: 2,
Reason: "TestReason-2abcd",
Message: "TestMessage-2abcd",
},
"4a333": {
Pid: 3,
CreatedAt: time.Now().UnixNano(),
StartedAt: time.Now().UnixNano(),
FinishedAt: time.Now().UnixNano(),
ExitCode: 3,
Reason: "TestReason-4a333",
Message: "TestMessage-4a333",
Starting: true,
},
"4abcd": {
Pid: 4,
CreatedAt: time.Now().UnixNano(),
StartedAt: time.Now().UnixNano(),
FinishedAt: time.Now().UnixNano(),
ExitCode: 4,
Reason: "TestReason-4abcd",
Message: "TestMessage-4abcd",
Removing: true,
},
}
assert := assertlib.New(t)
containers := map[string]Container{}
for id := range metadatas {
container, err := NewContainer(
metadatas[id],
WithFakeStatus(statuses[id]),
)
assert.NoError(err)
containers[id] = container
}
s := NewStore(label.NewStore())
reserved := map[string]bool{}
s.labels.Reserver = func(label string) {
reserved[strings.SplitN(label, ":", 4)[3]] = true
}
s.labels.Releaser = func(label string) {
reserved[strings.SplitN(label, ":", 4)[3]] = false
}
t.Logf("should be able to add container")
for _, c := range containers {
assert.NoError(s.Add(c))
}
t.Logf("should be able to get container")
genTruncIndex := func(normalName string) string { return normalName[:(len(normalName)+1)/2] }
for id, c := range containers {
got, err := s.Get(genTruncIndex(id))
assert.NoError(err)
assert.Equal(c, got)
}
t.Logf("should be able to list containers")
cs := s.List()
assert.Len(cs, len(containers))
if selinux.GetEnabled() {
t.Logf("should have reserved labels (requires -tag selinux)")
assert.Equal(map[string]bool{
"c1,c2": true,
"c1,c3": true,
"c1,c4": true,
}, reserved)
}
cntrNum := len(containers)
for testID, v := range containers {
truncID := genTruncIndex(testID)
t.Logf("add should return already exists error for duplicated container")
assert.Equal(store.ErrAlreadyExist, s.Add(v))
t.Logf("should be able to delete container")
s.Delete(truncID)
cntrNum--
cs = s.List()
assert.Len(cs, cntrNum)
t.Logf("get should return not exist error after deletion")
c, err := s.Get(truncID)
assert.Equal(Container{}, c)
assert.Equal(store.ErrNotExist, err)
}
if selinux.GetEnabled() {
t.Logf("should have released all labels (requires -tag selinux)")
assert.Equal(map[string]bool{
"c1,c2": false,
"c1,c3": false,
"c1,c4": false,
}, reserved)
}
}
func TestWithContainerIO(t *testing.T) {
meta := Metadata{
ID: "1",
Name: "Container-1",
SandboxID: "Sandbox-1",
Config: &runtime.ContainerConfig{
Metadata: &runtime.ContainerMetadata{
Name: "TestPod-1",
Attempt: 1,
},
},
ImageRef: "TestImage-1",
StopSignal: "SIGTERM",
LogPath: "/test/log/path",
}
status := Status{
Pid: 1,
CreatedAt: time.Now().UnixNano(),
StartedAt: time.Now().UnixNano(),
FinishedAt: time.Now().UnixNano(),
ExitCode: 1,
Reason: "TestReason-1",
Message: "TestMessage-1",
}
assert := assertlib.New(t)
c, err := NewContainer(meta, WithFakeStatus(status))
assert.NoError(err)
assert.Nil(c.IO)
c, err = NewContainer(
meta,
WithFakeStatus(status),
WithContainerIO(&cio.ContainerIO{}),
)
assert.NoError(err)
assert.NotNil(c.IO)
}

View File

@@ -0,0 +1,62 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package container
import "sync"
// WithFakeStatus adds fake status to the container.
func WithFakeStatus(status Status) Opts {
return func(c *Container) error {
c.Status = &fakeStatusStorage{status: status}
if status.FinishedAt != 0 {
// Fake the TaskExit event
c.Stop()
}
return nil
}
}
// fakeStatusStorage is a fake status storage for testing.
type fakeStatusStorage struct {
sync.RWMutex
status Status
}
func (f *fakeStatusStorage) Get() Status {
f.RLock()
defer f.RUnlock()
return f.status
}
func (f *fakeStatusStorage) UpdateSync(u UpdateFunc) error {
return f.Update(u)
}
func (f *fakeStatusStorage) Update(u UpdateFunc) error {
f.Lock()
defer f.Unlock()
newStatus, err := u(f.status)
if err != nil {
return err
}
f.status = newStatus
return nil
}
func (f *fakeStatusStorage) Delete() error {
return nil
}

View File

@@ -0,0 +1,89 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package container
import (
"encoding/json"
"github.com/pkg/errors"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
)
// NOTE(random-liu):
// 1) Metadata is immutable after created.
// 2) Metadata is checkpointed as containerd container label.
// metadataVersion is current version of container metadata.
const metadataVersion = "v1" // nolint
// versionedMetadata is the internal versioned container metadata.
// nolint
type versionedMetadata struct {
// Version indicates the version of the versioned container metadata.
Version string
// Metadata's type is metadataInternal. If not there will be a recursive call in MarshalJSON.
Metadata metadataInternal
}
// metadataInternal is for internal use.
type metadataInternal Metadata
// Metadata is the unversioned container metadata.
type Metadata struct {
// ID is the container id.
ID string
// Name is the container name.
Name string
// SandboxID is the sandbox id the container belongs to.
SandboxID string
// Config is the CRI container config.
// NOTE(random-liu): Resource limits are updatable, the source
// of truth for resource limits are in containerd.
Config *runtime.ContainerConfig
// ImageRef is the reference of image used by the container.
ImageRef string
// LogPath is the container log path.
LogPath string
// StopSignal is the system call signal that will be sent to the container to exit.
// TODO(random-liu): Add integration test for stop signal.
StopSignal string
// ProcessLabel is the SELinux process label for the container
ProcessLabel string
}
// MarshalJSON encodes Metadata into bytes in json format.
func (c *Metadata) MarshalJSON() ([]byte, error) {
return json.Marshal(&versionedMetadata{
Version: metadataVersion,
Metadata: metadataInternal(*c),
})
}
// UnmarshalJSON decodes Metadata from bytes.
func (c *Metadata) UnmarshalJSON(data []byte) error {
versioned := &versionedMetadata{}
if err := json.Unmarshal(data, versioned); err != nil {
return err
}
// Handle old version after upgrade.
switch versioned.Version {
case metadataVersion:
*c = Metadata(versioned.Metadata)
return nil
}
return errors.Errorf("unsupported version: %q", versioned.Version)
}

View File

@@ -0,0 +1,81 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package container
import (
"encoding/json"
"testing"
assertlib "github.com/stretchr/testify/assert"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
)
func TestMetadataMarshalUnmarshal(t *testing.T) {
meta := &Metadata{
ID: "test-id",
Name: "test-name",
SandboxID: "test-sandbox-id",
Config: &runtime.ContainerConfig{
Metadata: &runtime.ContainerMetadata{
Name: "test-name",
Attempt: 1,
},
},
ImageRef: "test-image-ref",
LogPath: "/test/log/path",
}
assert := assertlib.New(t)
newMeta := &Metadata{}
newVerMeta := &versionedMetadata{}
t.Logf("should be able to do json.marshal")
data, err := json.Marshal(meta)
assert.NoError(err)
data1, err := json.Marshal(&versionedMetadata{
Version: metadataVersion,
Metadata: metadataInternal(*meta),
})
assert.NoError(err)
assert.Equal(data, data1)
t.Logf("should be able to do MarshalJSON")
data, err = meta.MarshalJSON()
assert.NoError(err)
assert.NoError(newMeta.UnmarshalJSON(data))
assert.Equal(meta, newMeta)
t.Logf("should be able to do MarshalJSON and json.Unmarshal")
data, err = meta.MarshalJSON()
assert.NoError(err)
assert.NoError(json.Unmarshal(data, newVerMeta))
assert.Equal(meta, (*Metadata)(&newVerMeta.Metadata))
t.Logf("should be able to do json.Marshal and UnmarshalJSON")
data, err = json.Marshal(meta)
assert.NoError(err)
assert.NoError(newMeta.UnmarshalJSON(data))
assert.Equal(meta, newMeta)
t.Logf("should json.Unmarshal fail for unsupported version")
unsupported, err := json.Marshal(&versionedMetadata{
Version: "random-test-version",
Metadata: metadataInternal(*meta),
})
assert.NoError(err)
assert.Error(json.Unmarshal(unsupported, &newMeta))
}

View File

@@ -0,0 +1,247 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package container
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"sync"
"github.com/containerd/continuity"
"github.com/pkg/errors"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
)
// The container state machine in the CRI plugin:
//
// + +
// | |
// | Create | Load
// | |
// +----v----+ |
// | | |
// | CREATED <---------+-----------+
// | | | |
// +----+----- | |
// | | |
// | Start | |
// | | |
// +----v----+ | |
// Exec +--------+ | | |
// Attach | | RUNNING <---------+ |
// LogReopen +--------> | | |
// +----+----+ | |
// | | |
// | Stop/Exit | |
// | | |
// +----v----+ | |
// | <---------+ +----v----+
// | EXITED | | |
// | <----------------+ UNKNOWN |
// +----+----+ Stop | |
// | +---------+
// | Remove
// v
// DELETED
// statusVersion is current version of container status.
const statusVersion = "v1" // nolint
// versionedStatus is the internal used versioned container status.
// nolint
type versionedStatus struct {
// Version indicates the version of the versioned container status.
Version string
Status
}
// Status is the status of a container.
type Status struct {
// Pid is the init process id of the container.
Pid uint32
// CreatedAt is the created timestamp.
CreatedAt int64
// StartedAt is the started timestamp.
StartedAt int64
// FinishedAt is the finished timestamp.
FinishedAt int64
// ExitCode is the container exit code.
ExitCode int32
// CamelCase string explaining why container is in its current state.
Reason string
// Human-readable message indicating details about why container is in its
// current state.
Message string
// Starting indicates that the container is in starting state.
// This field doesn't need to be checkpointed.
Starting bool `json:"-"`
// Removing indicates that the container is in removing state.
// This field doesn't need to be checkpointed.
Removing bool `json:"-"`
// Unknown indicates that the container status is not fully loaded.
// This field doesn't need to be checkpointed.
Unknown bool `json:"-"`
}
// State returns current state of the container based on the container status.
func (s Status) State() runtime.ContainerState {
if s.Unknown {
return runtime.ContainerState_CONTAINER_UNKNOWN
}
if s.FinishedAt != 0 {
return runtime.ContainerState_CONTAINER_EXITED
}
if s.StartedAt != 0 {
return runtime.ContainerState_CONTAINER_RUNNING
}
if s.CreatedAt != 0 {
return runtime.ContainerState_CONTAINER_CREATED
}
return runtime.ContainerState_CONTAINER_UNKNOWN
}
// encode encodes Status into bytes in json format.
func (s *Status) encode() ([]byte, error) {
return json.Marshal(&versionedStatus{
Version: statusVersion,
Status: *s,
})
}
// decode decodes Status from bytes.
func (s *Status) decode(data []byte) error {
versioned := &versionedStatus{}
if err := json.Unmarshal(data, versioned); err != nil {
return err
}
// Handle old version after upgrade.
switch versioned.Version {
case statusVersion:
*s = versioned.Status
return nil
}
return errors.New("unsupported version")
}
// UpdateFunc is function used to update the container status. If there
// is an error, the update will be rolled back.
type UpdateFunc func(Status) (Status, error)
// StatusStorage manages the container status with a storage backend.
type StatusStorage interface {
// Get a container status.
Get() Status
// UpdateSync updates the container status and the on disk checkpoint.
// Note that the update MUST be applied in one transaction.
UpdateSync(UpdateFunc) error
// Update the container status. Note that the update MUST be applied
// in one transaction.
Update(UpdateFunc) error
// Delete the container status.
// Note:
// * Delete should be idempotent.
// * The status must be deleted in one trasaction.
Delete() error
}
// StoreStatus creates the storage containing the passed in container status with the
// specified id.
// The status MUST be created in one transaction.
func StoreStatus(root, id string, status Status) (StatusStorage, error) {
data, err := status.encode()
if err != nil {
return nil, errors.Wrap(err, "failed to encode status")
}
path := filepath.Join(root, "status")
if err := continuity.AtomicWriteFile(path, data, 0600); err != nil {
return nil, errors.Wrapf(err, "failed to checkpoint status to %q", path)
}
return &statusStorage{
path: path,
status: status,
}, nil
}
// LoadStatus loads container status from checkpoint. There shouldn't be threads
// writing to the file during loading.
func LoadStatus(root, id string) (Status, error) {
path := filepath.Join(root, "status")
data, err := ioutil.ReadFile(path)
if err != nil {
return Status{}, errors.Wrapf(err, "failed to read status from %q", path)
}
var status Status
if err := status.decode(data); err != nil {
return Status{}, errors.Wrapf(err, "failed to decode status %q", data)
}
return status, nil
}
type statusStorage struct {
sync.RWMutex
path string
status Status
}
// Get a copy of container status.
func (s *statusStorage) Get() Status {
s.RLock()
defer s.RUnlock()
return s.status
}
// UpdateSync updates the container status and the on disk checkpoint.
func (s *statusStorage) UpdateSync(u UpdateFunc) error {
s.Lock()
defer s.Unlock()
newStatus, err := u(s.status)
if err != nil {
return err
}
data, err := newStatus.encode()
if err != nil {
return errors.Wrap(err, "failed to encode status")
}
if err := continuity.AtomicWriteFile(s.path, data, 0600); err != nil {
return errors.Wrapf(err, "failed to checkpoint status to %q", s.path)
}
s.status = newStatus
return nil
}
// Update the container status.
func (s *statusStorage) Update(u UpdateFunc) error {
s.Lock()
defer s.Unlock()
newStatus, err := u(s.status)
if err != nil {
return err
}
s.status = newStatus
return nil
}
// Delete deletes the container status from disk atomically.
func (s *statusStorage) Delete() error {
temp := filepath.Dir(s.path) + ".del-" + filepath.Base(s.path)
if err := os.Rename(s.path, temp); err != nil && !os.IsNotExist(err) {
return err
}
return os.RemoveAll(temp)
}

View File

@@ -0,0 +1,195 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package container
import (
"encoding/json"
"errors"
"io/ioutil"
"os"
"path/filepath"
"testing"
"time"
assertlib "github.com/stretchr/testify/assert"
requirelib "github.com/stretchr/testify/require"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
)
func TestContainerState(t *testing.T) {
for c, test := range map[string]struct {
status Status
state runtime.ContainerState
}{
"unknown state": {
status: Status{
Unknown: true,
},
state: runtime.ContainerState_CONTAINER_UNKNOWN,
},
"unknown state because there is no timestamp set": {
status: Status{},
state: runtime.ContainerState_CONTAINER_UNKNOWN,
},
"created state": {
status: Status{
CreatedAt: time.Now().UnixNano(),
},
state: runtime.ContainerState_CONTAINER_CREATED,
},
"running state": {
status: Status{
CreatedAt: time.Now().UnixNano(),
StartedAt: time.Now().UnixNano(),
},
state: runtime.ContainerState_CONTAINER_RUNNING,
},
"exited state": {
status: Status{
CreatedAt: time.Now().UnixNano(),
FinishedAt: time.Now().UnixNano(),
},
state: runtime.ContainerState_CONTAINER_EXITED,
},
} {
t.Logf("TestCase %q", c)
assertlib.Equal(t, test.state, test.status.State())
}
}
func TestStatusEncodeDecode(t *testing.T) {
s := &Status{
Pid: 1234,
CreatedAt: time.Now().UnixNano(),
StartedAt: time.Now().UnixNano(),
FinishedAt: time.Now().UnixNano(),
ExitCode: 1,
Reason: "test-reason",
Message: "test-message",
Removing: true,
Starting: true,
Unknown: true,
}
assert := assertlib.New(t)
data, err := s.encode()
assert.NoError(err)
newS := &Status{}
assert.NoError(newS.decode(data))
s.Removing = false // Removing should not be encoded.
s.Starting = false // Starting should not be encoded.
s.Unknown = false // Unknown should not be encoded.
assert.Equal(s, newS)
unsupported, err := json.Marshal(&versionedStatus{
Version: "random-test-version",
Status: *s,
})
assert.NoError(err)
assert.Error(newS.decode(unsupported))
}
func TestStatus(t *testing.T) {
testID := "test-id"
testStatus := Status{
CreatedAt: time.Now().UnixNano(),
}
updateStatus := Status{
CreatedAt: time.Now().UnixNano(),
StartedAt: time.Now().UnixNano(),
}
updateErr := errors.New("update error")
assert := assertlib.New(t)
require := requirelib.New(t)
tempDir, err := ioutil.TempDir(os.TempDir(), "status-test")
require.NoError(err)
defer os.RemoveAll(tempDir)
statusFile := filepath.Join(tempDir, "status")
t.Logf("simple store and get")
s, err := StoreStatus(tempDir, testID, testStatus)
assert.NoError(err)
old := s.Get()
assert.Equal(testStatus, old)
_, err = os.Stat(statusFile)
assert.NoError(err)
loaded, err := LoadStatus(tempDir, testID)
require.NoError(err)
assert.Equal(testStatus, loaded)
t.Logf("failed update should not take effect")
err = s.Update(func(o Status) (Status, error) {
o = updateStatus
return o, updateErr
})
assert.Equal(updateErr, err)
assert.Equal(testStatus, s.Get())
loaded, err = LoadStatus(tempDir, testID)
require.NoError(err)
assert.Equal(testStatus, loaded)
t.Logf("successful update should take effect but not checkpoint")
err = s.Update(func(o Status) (Status, error) {
o = updateStatus
return o, nil
})
assert.NoError(err)
assert.Equal(updateStatus, s.Get())
loaded, err = LoadStatus(tempDir, testID)
require.NoError(err)
assert.Equal(testStatus, loaded)
// Recover status.
assert.NoError(s.Update(func(o Status) (Status, error) {
o = testStatus
return o, nil
}))
t.Logf("failed update sync should not take effect")
err = s.UpdateSync(func(o Status) (Status, error) {
o = updateStatus
return o, updateErr
})
assert.Equal(updateErr, err)
assert.Equal(testStatus, s.Get())
loaded, err = LoadStatus(tempDir, testID)
require.NoError(err)
assert.Equal(testStatus, loaded)
t.Logf("successful update sync should take effect and checkpoint")
err = s.UpdateSync(func(o Status) (Status, error) {
o = updateStatus
return o, nil
})
assert.NoError(err)
assert.Equal(updateStatus, s.Get())
loaded, err = LoadStatus(tempDir, testID)
require.NoError(err)
assert.Equal(updateStatus, loaded)
t.Logf("successful update should not affect existing snapshot")
assert.Equal(testStatus, old)
t.Logf("delete status")
assert.NoError(s.Delete())
_, err = LoadStatus(tempDir, testID)
assert.Error(err)
_, err = os.Stat(statusFile)
assert.True(os.IsNotExist(err))
t.Logf("delete status should be idempotent")
assert.NoError(s.Delete())
}

33
pkg/cri/store/errors.go Normal file
View File

@@ -0,0 +1,33 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package store
import "github.com/containerd/containerd/errdefs"
var (
// ErrAlreadyExist is the error returned when data added in the store
// already exists.
//
// This error has been DEPRECATED and will be removed in 1.5. Please switch
// usage directly to `errdefs.ErrAlreadyExists`.
ErrAlreadyExist = errdefs.ErrAlreadyExists
// ErrNotExist is the error returned when data is not in the store.
//
// This error has been DEPRECATED and will be removed in 1.5. Please switch
// usage directly to `errdefs.ErrNotFound`.
ErrNotExist = errdefs.ErrNotFound
)

View File

@@ -0,0 +1,48 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package store
import (
"testing"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/containerd/containerd/errdefs"
)
func TestStoreErrAlreadyExistGRPCStatus(t *testing.T) {
err := errdefs.ToGRPC(ErrAlreadyExist)
s, ok := status.FromError(err)
if !ok {
t.Fatalf("failed to convert err: %v to status: %d", err, codes.AlreadyExists)
}
if s.Code() != codes.AlreadyExists {
t.Fatalf("expected code: %d got: %d", codes.AlreadyExists, s.Code())
}
}
func TestStoreErrNotExistGRPCStatus(t *testing.T) {
err := errdefs.ToGRPC(ErrNotExist)
s, ok := status.FromError(err)
if !ok {
t.Fatalf("failed to convert err: %v to status: %d", err, codes.NotFound)
}
if s.Code() != codes.NotFound {
t.Fatalf("expected code: %d got: %d", codes.NotFound, s.Code())
}
}

View File

@@ -0,0 +1,34 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package image
import "github.com/pkg/errors"
// NewFakeStore returns an image store with predefined images.
// Update is not allowed for this fake store.
func NewFakeStore(images []Image) (*Store, error) {
s := NewStore(nil)
for _, i := range images {
for _, ref := range i.References {
s.refCache[ref] = i.ID
}
if err := s.store.add(i); err != nil {
return nil, errors.Wrapf(err, "add image %+v", i)
}
}
return s, nil
}

View File

@@ -0,0 +1,256 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package image
import (
"context"
"encoding/json"
"sync"
"github.com/containerd/containerd"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
imagedigest "github.com/opencontainers/go-digest"
"github.com/opencontainers/go-digest/digestset"
imageidentity "github.com/opencontainers/image-spec/identity"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
storeutil "github.com/containerd/containerd/pkg/cri/store"
"github.com/containerd/containerd/pkg/util"
)
// Image contains all resources associated with the image. All fields
// MUST not be mutated directly after created.
type Image struct {
// Id of the image. Normally the digest of image config.
ID string
// References are references to the image, e.g. RepoTag and RepoDigest.
References []string
// ChainID is the chainID of the image.
ChainID string
// Size is the compressed size of the image.
Size int64
// ImageSpec is the oci image structure which describes basic information about the image.
ImageSpec imagespec.Image
}
// Store stores all images.
type Store struct {
lock sync.RWMutex
// refCache is a containerd image reference to image id cache.
refCache map[string]string
// client is the containerd client.
client *containerd.Client
// store is the internal image store indexed by image id.
store *store
}
// NewStore creates an image store.
func NewStore(client *containerd.Client) *Store {
return &Store{
refCache: make(map[string]string),
client: client,
store: &store{
images: make(map[string]Image),
digestSet: digestset.NewSet(),
},
}
}
// Update updates cache for a reference.
func (s *Store) Update(ctx context.Context, ref string) error {
s.lock.Lock()
defer s.lock.Unlock()
i, err := s.client.GetImage(ctx, ref)
if err != nil && !errdefs.IsNotFound(err) {
return errors.Wrap(err, "get image from containerd")
}
var img *Image
if err == nil {
img, err = getImage(ctx, i)
if err != nil {
return errors.Wrap(err, "get image info from containerd")
}
}
return s.update(ref, img)
}
// update updates the internal cache. img == nil means that
// the image does not exist in containerd.
func (s *Store) update(ref string, img *Image) error {
oldID, oldExist := s.refCache[ref]
if img == nil {
// The image reference doesn't exist in containerd.
if oldExist {
// Remove the reference from the store.
s.store.delete(oldID, ref)
delete(s.refCache, ref)
}
return nil
}
if oldExist {
if oldID == img.ID {
return nil
}
// Updated. Remove tag from old image.
s.store.delete(oldID, ref)
}
// New image. Add new image.
s.refCache[ref] = img.ID
return s.store.add(*img)
}
// getImage gets image information from containerd.
func getImage(ctx context.Context, i containerd.Image) (*Image, error) {
// Get image information.
diffIDs, err := i.RootFS(ctx)
if err != nil {
return nil, errors.Wrap(err, "get image diffIDs")
}
chainID := imageidentity.ChainID(diffIDs)
size, err := i.Size(ctx)
if err != nil {
return nil, errors.Wrap(err, "get image compressed resource size")
}
desc, err := i.Config(ctx)
if err != nil {
return nil, errors.Wrap(err, "get image config descriptor")
}
id := desc.Digest.String()
rb, err := content.ReadBlob(ctx, i.ContentStore(), desc)
if err != nil {
return nil, errors.Wrap(err, "read image config from content store")
}
var ociimage imagespec.Image
if err := json.Unmarshal(rb, &ociimage); err != nil {
return nil, errors.Wrapf(err, "unmarshal image config %s", rb)
}
return &Image{
ID: id,
References: []string{i.Name()},
ChainID: chainID.String(),
Size: size,
ImageSpec: ociimage,
}, nil
}
// Resolve resolves a image reference to image id.
func (s *Store) Resolve(ref string) (string, error) {
s.lock.RLock()
defer s.lock.RUnlock()
id, ok := s.refCache[ref]
if !ok {
return "", storeutil.ErrNotExist
}
return id, nil
}
// Get gets image metadata by image id. The id can be truncated.
// Returns various validation errors if the image id is invalid.
// Returns storeutil.ErrNotExist if the image doesn't exist.
func (s *Store) Get(id string) (Image, error) {
return s.store.get(id)
}
// List lists all images.
func (s *Store) List() []Image {
return s.store.list()
}
type store struct {
lock sync.RWMutex
images map[string]Image
digestSet *digestset.Set
}
func (s *store) list() []Image {
s.lock.RLock()
defer s.lock.RUnlock()
var images []Image
for _, i := range s.images {
images = append(images, i)
}
return images
}
func (s *store) add(img Image) error {
s.lock.Lock()
defer s.lock.Unlock()
if _, err := s.digestSet.Lookup(img.ID); err != nil {
if err != digestset.ErrDigestNotFound {
return err
}
if err := s.digestSet.Add(imagedigest.Digest(img.ID)); err != nil {
return err
}
}
i, ok := s.images[img.ID]
if !ok {
// If the image doesn't exist, add it.
s.images[img.ID] = img
return nil
}
// Or else, merge the references.
i.References = util.MergeStringSlices(i.References, img.References)
s.images[img.ID] = i
return nil
}
func (s *store) get(id string) (Image, error) {
s.lock.RLock()
defer s.lock.RUnlock()
digest, err := s.digestSet.Lookup(id)
if err != nil {
if err == digestset.ErrDigestNotFound {
err = storeutil.ErrNotExist
}
return Image{}, err
}
if i, ok := s.images[digest.String()]; ok {
return i, nil
}
return Image{}, storeutil.ErrNotExist
}
func (s *store) delete(id, ref string) {
s.lock.Lock()
defer s.lock.Unlock()
digest, err := s.digestSet.Lookup(id)
if err != nil {
// Note: The idIndex.Delete and delete doesn't handle truncated index.
// So we need to return if there are error.
return
}
i, ok := s.images[digest.String()]
if !ok {
return
}
i.References = util.SubtractStringSlice(i.References, ref)
if len(i.References) != 0 {
s.images[digest.String()] = i
return
}
// Remove the image if it is not referenced any more.
s.digestSet.Remove(digest) // nolint: errcheck
delete(s.images, digest.String())
}

View File

@@ -0,0 +1,248 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package image
import (
"sort"
"strings"
"testing"
"github.com/opencontainers/go-digest/digestset"
assertlib "github.com/stretchr/testify/assert"
storeutil "github.com/containerd/containerd/pkg/cri/store"
)
func TestInternalStore(t *testing.T) {
images := []Image{
{
ID: "sha256:1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
ChainID: "test-chain-id-1",
References: []string{"ref-1"},
Size: 10,
},
{
ID: "sha256:2123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
ChainID: "test-chain-id-2abcd",
References: []string{"ref-2abcd"},
Size: 20,
},
{
ID: "sha256:3123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
References: []string{"ref-4a333"},
ChainID: "test-chain-id-4a333",
Size: 30,
},
{
ID: "sha256:4123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
References: []string{"ref-4abcd"},
ChainID: "test-chain-id-4abcd",
Size: 40,
},
}
assert := assertlib.New(t)
genTruncIndex := func(normalName string) string { return normalName[:(len(normalName)+1)/2] }
s := &store{
images: make(map[string]Image),
digestSet: digestset.NewSet(),
}
t.Logf("should be able to add image")
for _, img := range images {
err := s.add(img)
assert.NoError(err)
}
t.Logf("should be able to get image")
for _, v := range images {
truncID := genTruncIndex(v.ID)
got, err := s.get(truncID)
assert.NoError(err, "truncID:%s, fullID:%s", truncID, v.ID)
assert.Equal(v, got)
}
t.Logf("should be able to get image by truncated imageId without algorithm")
for _, v := range images {
truncID := genTruncIndex(v.ID[strings.Index(v.ID, ":")+1:])
got, err := s.get(truncID)
assert.NoError(err, "truncID:%s, fullID:%s", truncID, v.ID)
assert.Equal(v, got)
}
t.Logf("should not be able to get image by ambiguous prefix")
ambiguousPrefixs := []string{"sha256", "sha256:"}
for _, v := range ambiguousPrefixs {
_, err := s.get(v)
assert.NotEqual(nil, err)
}
t.Logf("should be able to list images")
imgs := s.list()
assert.Len(imgs, len(images))
imageNum := len(images)
for _, v := range images {
truncID := genTruncIndex(v.ID)
oldRef := v.References[0]
newRef := oldRef + "new"
t.Logf("should be able to add new references")
newImg := v
newImg.References = []string{newRef}
err := s.add(newImg)
assert.NoError(err)
got, err := s.get(truncID)
assert.NoError(err)
assert.Len(got.References, 2)
assert.Contains(got.References, oldRef, newRef)
t.Logf("should not be able to add duplicated references")
err = s.add(newImg)
assert.NoError(err)
got, err = s.get(truncID)
assert.NoError(err)
assert.Len(got.References, 2)
assert.Contains(got.References, oldRef, newRef)
t.Logf("should be able to delete image references")
s.delete(truncID, oldRef)
got, err = s.get(truncID)
assert.NoError(err)
assert.Equal([]string{newRef}, got.References)
t.Logf("should be able to delete image")
s.delete(truncID, newRef)
got, err = s.get(truncID)
assert.Equal(storeutil.ErrNotExist, err)
assert.Equal(Image{}, got)
imageNum--
imgs = s.list()
assert.Len(imgs, imageNum)
}
}
func TestImageStore(t *testing.T) {
id := "sha256:1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
newID := "sha256:9923456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
image := Image{
ID: id,
ChainID: "test-chain-id-1",
References: []string{"ref-1"},
Size: 10,
}
assert := assertlib.New(t)
equal := func(i1, i2 Image) {
sort.Strings(i1.References)
sort.Strings(i2.References)
assert.Equal(i1, i2)
}
for desc, test := range map[string]struct {
ref string
image *Image
expected []Image
}{
"nothing should happen if a non-exist ref disappear": {
ref: "ref-2",
image: nil,
expected: []Image{image},
},
"new ref for an existing image": {
ref: "ref-2",
image: &Image{
ID: id,
ChainID: "test-chain-id-1",
References: []string{"ref-2"},
Size: 10,
},
expected: []Image{
{
ID: id,
ChainID: "test-chain-id-1",
References: []string{"ref-1", "ref-2"},
Size: 10,
},
},
},
"new ref for a new image": {
ref: "ref-2",
image: &Image{
ID: newID,
ChainID: "test-chain-id-2",
References: []string{"ref-2"},
Size: 20,
},
expected: []Image{
image,
{
ID: newID,
ChainID: "test-chain-id-2",
References: []string{"ref-2"},
Size: 20,
},
},
},
"existing ref point to a new image": {
ref: "ref-1",
image: &Image{
ID: newID,
ChainID: "test-chain-id-2",
References: []string{"ref-1"},
Size: 20,
},
expected: []Image{
{
ID: newID,
ChainID: "test-chain-id-2",
References: []string{"ref-1"},
Size: 20,
},
},
},
"existing ref disappear": {
ref: "ref-1",
image: nil,
expected: []Image{},
},
} {
t.Logf("TestCase %q", desc)
s, err := NewFakeStore([]Image{image})
assert.NoError(err)
assert.NoError(s.update(test.ref, test.image))
assert.Len(s.List(), len(test.expected))
for _, expect := range test.expected {
got, err := s.Get(expect.ID)
assert.NoError(err)
equal(got, expect)
for _, ref := range expect.References {
id, err := s.Resolve(ref)
assert.NoError(err)
assert.Equal(expect.ID, id)
}
}
if test.image == nil {
// Shouldn't be able to index by removed ref.
id, err := s.Resolve(test.ref)
assert.Equal(storeutil.ErrNotExist, err)
assert.Empty(id)
}
}
}

View File

@@ -0,0 +1,90 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package label
import (
"sync"
"github.com/opencontainers/selinux/go-selinux"
)
type Store struct {
sync.Mutex
levels map[string]int
Releaser func(string)
Reserver func(string)
}
func NewStore() *Store {
return &Store{
levels: map[string]int{},
Releaser: selinux.ReleaseLabel,
Reserver: selinux.ReserveLabel,
}
}
func (s *Store) Reserve(label string) error {
s.Lock()
defer s.Unlock()
context, err := selinux.NewContext(label)
if err != nil {
return err
}
level := context["level"]
// no reason to count empty
if level == "" {
return nil
}
if _, ok := s.levels[level]; !ok {
s.Reserver(label)
}
s.levels[level]++
return nil
}
func (s *Store) Release(label string) {
s.Lock()
defer s.Unlock()
context, err := selinux.NewContext(label)
if err != nil {
return
}
level := context["level"]
if level == "" {
return
}
count, ok := s.levels[level]
if !ok {
return
}
switch {
case count == 1:
s.Releaser(label)
delete(s.levels, level)
case count < 1:
delete(s.levels, level)
case count > 1:
s.levels[level] = count - 1
}
}

View File

@@ -0,0 +1,116 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package label
import (
"testing"
"github.com/opencontainers/selinux/go-selinux"
"github.com/stretchr/testify/assert"
)
func TestAddThenRemove(t *testing.T) {
if !selinux.GetEnabled() {
t.Skip("selinux is not enabled")
}
assert := assert.New(t)
store := NewStore()
releaseCount := 0
reserveCount := 0
store.Releaser = func(label string) {
assert.Contains(label, ":c1,c2")
releaseCount++
assert.Equal(1, releaseCount)
}
store.Reserver = func(label string) {
assert.Contains(label, ":c1,c2")
reserveCount++
assert.Equal(1, reserveCount)
}
t.Log("should count to two level")
assert.NoError(store.Reserve("junk:junk:junk:c1,c2"))
assert.NoError(store.Reserve("junk2:junk2:junk2:c1,c2"))
t.Log("should have one item")
assert.Equal(1, len(store.levels))
t.Log("c1,c2 count should be 2")
assert.Equal(2, store.levels["c1,c2"])
store.Release("junk:junk:junk:c1,c2")
store.Release("junk2:junk2:junk2:c1,c2")
t.Log("should have 0 items")
assert.Equal(0, len(store.levels))
t.Log("should have reserved")
assert.Equal(1, reserveCount)
t.Log("should have released")
assert.Equal(1, releaseCount)
}
func TestJunkData(t *testing.T) {
if !selinux.GetEnabled() {
t.Skip("selinux is not enabled")
}
assert := assert.New(t)
store := NewStore()
releaseCount := 0
store.Releaser = func(label string) {
releaseCount++
}
reserveCount := 0
store.Reserver = func(label string) {
reserveCount++
}
t.Log("should ignore empty label")
assert.NoError(store.Reserve(""))
assert.Equal(0, len(store.levels))
store.Release("")
assert.Equal(0, len(store.levels))
assert.Equal(0, releaseCount)
assert.Equal(0, reserveCount)
t.Log("should fail on bad label")
assert.Error(store.Reserve("junkjunkjunkc1c2"))
assert.Equal(0, len(store.levels))
store.Release("junkjunkjunkc1c2")
assert.Equal(0, len(store.levels))
assert.Equal(0, releaseCount)
assert.Equal(0, reserveCount)
t.Log("should not release unknown label")
store.Release("junk2:junk2:junk2:c1,c2")
assert.Equal(0, len(store.levels))
assert.Equal(0, releaseCount)
assert.Equal(0, reserveCount)
t.Log("should release once even if too many deletes")
assert.NoError(store.Reserve("junk2:junk2:junk2:c1,c2"))
assert.Equal(1, len(store.levels))
assert.Equal(1, store.levels["c1,c2"])
store.Release("junk2:junk2:junk2:c1,c2")
store.Release("junk2:junk2:junk2:c1,c2")
assert.Equal(0, len(store.levels))
assert.Equal(1, releaseCount)
assert.Equal(1, reserveCount)
}

View File

@@ -0,0 +1,89 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sandbox
import (
"encoding/json"
cni "github.com/containerd/go-cni"
"github.com/pkg/errors"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
)
// NOTE(random-liu):
// 1) Metadata is immutable after created.
// 2) Metadata is checkpointed as containerd container label.
// metadataVersion is current version of sandbox metadata.
const metadataVersion = "v1" // nolint
// versionedMetadata is the internal versioned sandbox metadata.
// nolint
type versionedMetadata struct {
// Version indicates the version of the versioned sandbox metadata.
Version string
// Metadata's type is metadataInternal. If not there will be a recursive call in MarshalJSON.
Metadata metadataInternal
}
// metadataInternal is for internal use.
type metadataInternal Metadata
// Metadata is the unversioned sandbox metadata.
type Metadata struct {
// ID is the sandbox id.
ID string
// Name is the sandbox name.
Name string
// Config is the CRI sandbox config.
Config *runtime.PodSandboxConfig
// NetNSPath is the network namespace used by the sandbox.
NetNSPath string
// IP of Pod if it is attached to non host network
IP string
// AdditionalIPs of the Pod if it is attached to non host network
AdditionalIPs []string
// RuntimeHandler is the runtime handler name of the pod.
RuntimeHandler string
// CNIresult resulting configuration for attached network namespace interfaces
CNIResult *cni.CNIResult
// ProcessLabel is the SELinux process label for the container
ProcessLabel string
}
// MarshalJSON encodes Metadata into bytes in json format.
func (c *Metadata) MarshalJSON() ([]byte, error) {
return json.Marshal(&versionedMetadata{
Version: metadataVersion,
Metadata: metadataInternal(*c),
})
}
// UnmarshalJSON decodes Metadata from bytes.
func (c *Metadata) UnmarshalJSON(data []byte) error {
versioned := &versionedMetadata{}
if err := json.Unmarshal(data, versioned); err != nil {
return err
}
// Handle old version after upgrade.
switch versioned.Version {
case metadataVersion:
*c = Metadata(versioned.Metadata)
return nil
}
return errors.Errorf("unsupported version: %q", versioned.Version)
}

View File

@@ -0,0 +1,79 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sandbox
import (
"encoding/json"
"testing"
assertlib "github.com/stretchr/testify/assert"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
)
func TestMetadataMarshalUnmarshal(t *testing.T) {
meta := &Metadata{
ID: "test-id",
Name: "test-name",
Config: &runtime.PodSandboxConfig{
Metadata: &runtime.PodSandboxMetadata{
Name: "test-name",
Uid: "test-uid",
Namespace: "test-namespace",
Attempt: 1,
},
},
}
assert := assertlib.New(t)
newMeta := &Metadata{}
newVerMeta := &versionedMetadata{}
t.Logf("should be able to do json.marshal")
data, err := json.Marshal(meta)
assert.NoError(err)
data1, err := json.Marshal(&versionedMetadata{
Version: metadataVersion,
Metadata: metadataInternal(*meta),
})
assert.NoError(err)
assert.Equal(data, data1)
t.Logf("should be able to do MarshalJSON")
data, err = meta.MarshalJSON()
assert.NoError(err)
assert.NoError(newMeta.UnmarshalJSON(data))
assert.Equal(meta, newMeta)
t.Logf("should be able to do MarshalJSON and json.Unmarshal")
data, err = meta.MarshalJSON()
assert.NoError(err)
assert.NoError(json.Unmarshal(data, newVerMeta))
assert.Equal(meta, (*Metadata)(&newVerMeta.Metadata))
t.Logf("should be able to do json.Marshal and UnmarshalJSON")
data, err = json.Marshal(meta)
assert.NoError(err)
assert.NoError(newMeta.UnmarshalJSON(data))
assert.Equal(meta, newMeta)
t.Logf("should json.Unmarshal fail for unsupported version")
unsupported, err := json.Marshal(&versionedMetadata{
Version: "random-test-version",
Metadata: metadataInternal(*meta),
})
assert.NoError(err)
assert.Error(json.Unmarshal(unsupported, &newMeta))
}

View File

@@ -0,0 +1,137 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sandbox
import (
"sync"
"github.com/containerd/containerd"
"github.com/containerd/containerd/pkg/cri/store/label"
"github.com/docker/docker/pkg/truncindex"
"github.com/containerd/containerd/pkg/cri/store"
"github.com/containerd/containerd/pkg/netns"
)
// Sandbox contains all resources associated with the sandbox. All methods to
// mutate the internal state are thread safe.
type Sandbox struct {
// Metadata is the metadata of the sandbox, it is immutable after created.
Metadata
// Status stores the status of the sandbox.
Status StatusStorage
// Container is the containerd sandbox container client.
Container containerd.Container
// CNI network namespace client.
// For hostnetwork pod, this is always nil;
// For non hostnetwork pod, this should never be nil.
NetNS *netns.NetNS
// StopCh is used to propagate the stop information of the sandbox.
*store.StopCh
}
// NewSandbox creates an internally used sandbox type. This functions reminds
// the caller that a sandbox must have a status.
func NewSandbox(metadata Metadata, status Status) Sandbox {
s := Sandbox{
Metadata: metadata,
Status: StoreStatus(status),
StopCh: store.NewStopCh(),
}
if status.State == StateNotReady {
s.Stop()
}
return s
}
// Store stores all sandboxes.
type Store struct {
lock sync.RWMutex
sandboxes map[string]Sandbox
idIndex *truncindex.TruncIndex
labels *label.Store
}
// NewStore creates a sandbox store.
func NewStore(labels *label.Store) *Store {
return &Store{
sandboxes: make(map[string]Sandbox),
idIndex: truncindex.NewTruncIndex([]string{}),
labels: labels,
}
}
// Add a sandbox into the store.
func (s *Store) Add(sb Sandbox) error {
s.lock.Lock()
defer s.lock.Unlock()
if _, ok := s.sandboxes[sb.ID]; ok {
return store.ErrAlreadyExist
}
if err := s.labels.Reserve(sb.ProcessLabel); err != nil {
return err
}
if err := s.idIndex.Add(sb.ID); err != nil {
return err
}
s.sandboxes[sb.ID] = sb
return nil
}
// Get returns the sandbox with specified id.
// Returns store.ErrNotExist if the sandbox doesn't exist.
func (s *Store) Get(id string) (Sandbox, error) {
s.lock.RLock()
defer s.lock.RUnlock()
id, err := s.idIndex.Get(id)
if err != nil {
if err == truncindex.ErrNotExist {
err = store.ErrNotExist
}
return Sandbox{}, err
}
if sb, ok := s.sandboxes[id]; ok {
return sb, nil
}
return Sandbox{}, store.ErrNotExist
}
// List lists all sandboxes.
func (s *Store) List() []Sandbox {
s.lock.RLock()
defer s.lock.RUnlock()
var sandboxes []Sandbox
for _, sb := range s.sandboxes {
sandboxes = append(sandboxes, sb)
}
return sandboxes
}
// Delete deletes the sandbox with specified id.
func (s *Store) Delete(id string) {
s.lock.Lock()
defer s.lock.Unlock()
id, err := s.idIndex.Get(id)
if err != nil {
// Note: The idIndex.Delete and delete doesn't handle truncated index.
// So we need to return if there are error.
return
}
s.labels.Release(s.sandboxes[id].ProcessLabel)
s.idIndex.Delete(id) // nolint: errcheck
delete(s.sandboxes, id)
}

View File

@@ -0,0 +1,156 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sandbox
import (
"testing"
"github.com/containerd/containerd/pkg/cri/store/label"
assertlib "github.com/stretchr/testify/assert"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
"github.com/containerd/containerd/pkg/cri/store"
)
func TestSandboxStore(t *testing.T) {
sandboxes := map[string]Sandbox{
"1": NewSandbox(
Metadata{
ID: "1",
Name: "Sandbox-1",
Config: &runtime.PodSandboxConfig{
Metadata: &runtime.PodSandboxMetadata{
Name: "TestPod-1",
Uid: "TestUid-1",
Namespace: "TestNamespace-1",
Attempt: 1,
},
},
NetNSPath: "TestNetNS-1",
},
Status{State: StateReady},
),
"2abcd": NewSandbox(
Metadata{
ID: "2abcd",
Name: "Sandbox-2abcd",
Config: &runtime.PodSandboxConfig{
Metadata: &runtime.PodSandboxMetadata{
Name: "TestPod-2abcd",
Uid: "TestUid-2abcd",
Namespace: "TestNamespace-2abcd",
Attempt: 2,
},
},
NetNSPath: "TestNetNS-2",
},
Status{State: StateNotReady},
),
"4a333": NewSandbox(
Metadata{
ID: "4a333",
Name: "Sandbox-4a333",
Config: &runtime.PodSandboxConfig{
Metadata: &runtime.PodSandboxMetadata{
Name: "TestPod-4a333",
Uid: "TestUid-4a333",
Namespace: "TestNamespace-4a333",
Attempt: 3,
},
},
NetNSPath: "TestNetNS-3",
},
Status{State: StateNotReady},
),
"4abcd": NewSandbox(
Metadata{
ID: "4abcd",
Name: "Sandbox-4abcd",
Config: &runtime.PodSandboxConfig{
Metadata: &runtime.PodSandboxMetadata{
Name: "TestPod-4abcd",
Uid: "TestUid-4abcd",
Namespace: "TestNamespace-4abcd",
Attempt: 1,
},
},
NetNSPath: "TestNetNS-4abcd",
},
Status{State: StateReady},
),
}
unknown := NewSandbox(
Metadata{
ID: "3defg",
Name: "Sandbox-3defg",
Config: &runtime.PodSandboxConfig{
Metadata: &runtime.PodSandboxMetadata{
Name: "TestPod-3defg",
Uid: "TestUid-3defg",
Namespace: "TestNamespace-3defg",
Attempt: 1,
},
},
NetNSPath: "TestNetNS-3defg",
},
Status{State: StateUnknown},
)
assert := assertlib.New(t)
s := NewStore(label.NewStore())
t.Logf("should be able to add sandbox")
for _, sb := range sandboxes {
assert.NoError(s.Add(sb))
}
assert.NoError(s.Add(unknown))
t.Logf("should be able to get sandbox")
genTruncIndex := func(normalName string) string { return normalName[:(len(normalName)+1)/2] }
for id, sb := range sandboxes {
got, err := s.Get(genTruncIndex(id))
assert.NoError(err)
assert.Equal(sb, got)
}
t.Logf("should be able to get sandbox in unknown state with Get")
got, err := s.Get(unknown.ID)
assert.NoError(err)
assert.Equal(unknown, got)
t.Logf("should be able to list sandboxes")
sbNum := len(sandboxes) + 1
sbs := s.List()
assert.Len(sbs, sbNum)
for testID, v := range sandboxes {
truncID := genTruncIndex(testID)
t.Logf("add should return already exists error for duplicated sandbox")
assert.Equal(store.ErrAlreadyExist, s.Add(v))
t.Logf("should be able to delete sandbox")
s.Delete(truncID)
sbNum--
sbs = s.List()
assert.Len(sbs, sbNum)
t.Logf("get should return not exist error after deletion")
sb, err := s.Get(truncID)
assert.Equal(Sandbox{}, sb)
assert.Equal(store.ErrNotExist, err)
}
}

View File

@@ -0,0 +1,151 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sandbox
import (
"strconv"
"sync"
"time"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
)
// The sandbox state machine in the CRI plugin:
// + +
// | |
// | Create(Run) | Load
// | |
// Start | |
// (failed) | |
// +------------------+ +-----------+
// | | | |
// | | | |
// | | | |
// | | Start(Run) | |
// | | | |
// | PortForward +----v----+ | |
// | +------+ | | |
// | | | READY <---------+ |
// | +------> | | |
// | +----+----+ | |
// | | | |
// | | Stop/Exit | |
// | | | |
// | +----v----+ | |
// | | <---------+ +----v----+
// | | NOTREADY| | |
// | | <----------------+ UNKNOWN |
// | +----+----+ Stop | |
// | | +---------+
// | | Remove
// | v
// +-------------> DELETED
// State is the sandbox state we use in containerd/cri.
// It includes unknown, which is internal states not defined in CRI.
// The state mapping from internal states to CRI states:
// * ready -> ready
// * not ready -> not ready
// * unknown -> not ready
type State uint32
const (
// StateReady is ready state, it means sandbox container
// is running.
StateReady State = iota
// StateNotReady is notready state, it ONLY means sandbox
// container is not running.
// StopPodSandbox should still be called for NOTREADY sandbox to
// cleanup resources other than sandbox container, e.g. network namespace.
// This is an assumption made in CRI.
StateNotReady
// StateUnknown is unknown state. Sandbox only goes
// into unknown state when its status fails to be loaded.
StateUnknown
)
// String returns the string representation of the state
func (s State) String() string {
switch s {
case StateReady:
return runtime.PodSandboxState_SANDBOX_READY.String()
case StateNotReady:
return runtime.PodSandboxState_SANDBOX_NOTREADY.String()
case StateUnknown:
// PodSandboxState doesn't have an unknown state, but State does, so return a string using the same convention
return "SANDBOX_UNKNOWN"
default:
return "invalid sandbox state value: " + strconv.Itoa(int(s))
}
}
// Status is the status of a sandbox.
type Status struct {
// Pid is the init process id of the sandbox container.
Pid uint32
// CreatedAt is the created timestamp.
CreatedAt time.Time
// State is the state of the sandbox.
State State
}
// UpdateFunc is function used to update the sandbox status. If there
// is an error, the update will be rolled back.
type UpdateFunc func(Status) (Status, error)
// StatusStorage manages the sandbox status.
// The status storage for sandbox is different from container status storage,
// because we don't checkpoint sandbox status. If we need checkpoint in the
// future, we should combine this with container status storage.
type StatusStorage interface {
// Get a sandbox status.
Get() Status
// Update the sandbox status. Note that the update MUST be applied
// in one transaction.
Update(UpdateFunc) error
}
// StoreStatus creates the storage containing the passed in sandbox status with the
// specified id.
// The status MUST be created in one transaction.
func StoreStatus(status Status) StatusStorage {
return &statusStorage{status: status}
}
type statusStorage struct {
sync.RWMutex
status Status
}
// Get a copy of sandbox status.
func (s *statusStorage) Get() Status {
s.RLock()
defer s.RUnlock()
return s.status
}
// Update the sandbox status.
func (s *statusStorage) Update(u UpdateFunc) error {
s.Lock()
defer s.Unlock()
newStatus, err := u(s.status)
if err != nil {
return err
}
s.status = newStatus
return nil
}

View File

@@ -0,0 +1,69 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package sandbox
import (
"errors"
"testing"
"time"
assertlib "github.com/stretchr/testify/assert"
)
func TestStatus(t *testing.T) {
testStatus := Status{
Pid: 123,
CreatedAt: time.Now(),
State: StateUnknown,
}
updateStatus := Status{
Pid: 456,
CreatedAt: time.Now(),
State: StateReady,
}
updateErr := errors.New("update error")
assert := assertlib.New(t)
t.Logf("simple store and get")
s := StoreStatus(testStatus)
old := s.Get()
assert.Equal(testStatus, old)
t.Logf("failed update should not take effect")
err := s.Update(func(o Status) (Status, error) {
o = updateStatus
return o, updateErr
})
assert.Equal(updateErr, err)
assert.Equal(testStatus, s.Get())
t.Logf("successful update should take effect but not checkpoint")
err = s.Update(func(o Status) (Status, error) {
o = updateStatus
return o, nil
})
assert.NoError(err)
assert.Equal(updateStatus, s.Get())
}
func TestStateStringConversion(t *testing.T) {
assert := assertlib.New(t)
assert.Equal("SANDBOX_READY", StateReady.String())
assert.Equal("SANDBOX_NOTREADY", StateNotReady.String())
assert.Equal("SANDBOX_UNKNOWN", StateUnknown.String())
assert.Equal("invalid sandbox state value: 123", State(123).String())
}

View File

@@ -0,0 +1,87 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package snapshot
import (
"sync"
snapshot "github.com/containerd/containerd/snapshots"
"github.com/containerd/containerd/pkg/cri/store"
)
// Snapshot contains the information about the snapshot.
type Snapshot struct {
// Key is the key of the snapshot
Key string
// Kind is the kind of the snapshot (active, committed, view)
Kind snapshot.Kind
// Size is the size of the snapshot in bytes.
Size uint64
// Inodes is the number of inodes used by the snapshot
Inodes uint64
// Timestamp is latest update time (in nanoseconds) of the snapshot
// information.
Timestamp int64
}
// Store stores all snapshots.
type Store struct {
lock sync.RWMutex
snapshots map[string]Snapshot
}
// NewStore creates a snapshot store.
func NewStore() *Store {
return &Store{snapshots: make(map[string]Snapshot)}
}
// Add a snapshot into the store.
func (s *Store) Add(snapshot Snapshot) {
s.lock.Lock()
defer s.lock.Unlock()
s.snapshots[snapshot.Key] = snapshot
}
// Get returns the snapshot with specified key. Returns store.ErrNotExist if the
// snapshot doesn't exist.
func (s *Store) Get(key string) (Snapshot, error) {
s.lock.RLock()
defer s.lock.RUnlock()
if sn, ok := s.snapshots[key]; ok {
return sn, nil
}
return Snapshot{}, store.ErrNotExist
}
// List lists all snapshots.
func (s *Store) List() []Snapshot {
s.lock.RLock()
defer s.lock.RUnlock()
var snapshots []Snapshot
for _, sn := range s.snapshots {
snapshots = append(snapshots, sn)
}
return snapshots
}
// Delete deletes the snapshot with specified key.
func (s *Store) Delete(key string) {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.snapshots, key)
}

View File

@@ -0,0 +1,84 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package snapshot
import (
"testing"
"time"
snapshot "github.com/containerd/containerd/snapshots"
assertlib "github.com/stretchr/testify/assert"
"github.com/containerd/containerd/pkg/cri/store"
)
func TestSnapshotStore(t *testing.T) {
snapshots := map[string]Snapshot{
"key1": {
Key: "key1",
Kind: snapshot.KindActive,
Size: 10,
Inodes: 100,
Timestamp: time.Now().UnixNano(),
},
"key2": {
Key: "key2",
Kind: snapshot.KindCommitted,
Size: 20,
Inodes: 200,
Timestamp: time.Now().UnixNano(),
},
"key3": {
Key: "key3",
Kind: snapshot.KindView,
Size: 0,
Inodes: 0,
Timestamp: time.Now().UnixNano(),
},
}
assert := assertlib.New(t)
s := NewStore()
t.Logf("should be able to add snapshot")
for _, sn := range snapshots {
s.Add(sn)
}
t.Logf("should be able to get snapshot")
for id, sn := range snapshots {
got, err := s.Get(id)
assert.NoError(err)
assert.Equal(sn, got)
}
t.Logf("should be able to list snapshot")
sns := s.List()
assert.Len(sns, 3)
testKey := "key2"
t.Logf("should be able to delete snapshot")
s.Delete(testKey)
sns = s.List()
assert.Len(sns, 2)
t.Logf("get should return empty struct and ErrNotExist after deletion")
sn, err := s.Get(testKey)
assert.Equal(Snapshot{}, sn)
assert.Equal(store.ErrNotExist, err)
}

42
pkg/cri/store/util.go Normal file
View File

@@ -0,0 +1,42 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package store
import "sync"
// StopCh is used to propagate the stop information of a container.
type StopCh struct {
ch chan struct{}
once sync.Once
}
// NewStopCh creates a stop channel. The channel is open by default.
func NewStopCh() *StopCh {
return &StopCh{ch: make(chan struct{})}
}
// Stop close stopCh of the container.
func (s *StopCh) Stop() {
s.once.Do(func() {
close(s.ch)
})
}
// Stopped return the stopCh of the container as a readonly channel.
func (s *StopCh) Stopped() <-chan struct{} {
return s.ch
}