1/*
2   Copyright The containerd Authors.
3
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7
8       http://www.apache.org/licenses/LICENSE-2.0
9
10   Unless required by applicable law or agreed to in writing, software
11   distributed under the License is distributed on an "AS IS" BASIS,
12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   See the License for the specific language governing permissions and
14   limitations under the License.
15*/
16
17package container
18
19import (
20	"encoding/json"
21	"io/ioutil"
22	"os"
23	"path/filepath"
24	"sync"
25
26	"github.com/containerd/continuity"
27	"github.com/pkg/errors"
28	runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
29)
30
31// The container state machine in the CRI plugin:
32//
33//                         +              +
34//                         |              |
35//                         | Create       | Load
36//                         |              |
37//                    +----v----+         |
38//                    |         |         |
39//                    | CREATED <---------+-----------+
40//                    |         |         |           |
41//                    +----+-----         |           |
42//                         |              |           |
43//                         | Start        |           |
44//                         |              |           |
45//                    +----v----+         |           |
46//   Exec    +--------+         |         |           |
47//  Attach   |        | RUNNING <---------+           |
48// LogReopen +-------->         |         |           |
49//                    +----+----+         |           |
50//                         |              |           |
51//                         | Stop/Exit    |           |
52//                         |              |           |
53//                    +----v----+         |           |
54//                    |         <---------+      +----v----+
55//                    |  EXITED |                |         |
56//                    |         <----------------+ UNKNOWN |
57//                    +----+----+       Stop     |         |
58//                         |                     +---------+
59//                         | Remove
60//                         v
61//                      DELETED
62
63// statusVersion is current version of container status.
64const statusVersion = "v1" // nolint
65
66// versionedStatus is the internal used versioned container status.
67// nolint
68type versionedStatus struct {
69	// Version indicates the version of the versioned container status.
70	Version string
71	Status
72}
73
74// Status is the status of a container.
75type Status struct {
76	// Pid is the init process id of the container.
77	Pid uint32
78	// CreatedAt is the created timestamp.
79	CreatedAt int64
80	// StartedAt is the started timestamp.
81	StartedAt int64
82	// FinishedAt is the finished timestamp.
83	FinishedAt int64
84	// ExitCode is the container exit code.
85	ExitCode int32
86	// CamelCase string explaining why container is in its current state.
87	Reason string
88	// Human-readable message indicating details about why container is in its
89	// current state.
90	Message string
91	// Starting indicates that the container is in starting state.
92	// This field doesn't need to be checkpointed.
93	Starting bool `json:"-"`
94	// Removing indicates that the container is in removing state.
95	// This field doesn't need to be checkpointed.
96	Removing bool `json:"-"`
97	// Unknown indicates that the container status is not fully loaded.
98	// This field doesn't need to be checkpointed.
99	Unknown bool `json:"-"`
100}
101
102// State returns current state of the container based on the container status.
103func (s Status) State() runtime.ContainerState {
104	if s.Unknown {
105		return runtime.ContainerState_CONTAINER_UNKNOWN
106	}
107	if s.FinishedAt != 0 {
108		return runtime.ContainerState_CONTAINER_EXITED
109	}
110	if s.StartedAt != 0 {
111		return runtime.ContainerState_CONTAINER_RUNNING
112	}
113	if s.CreatedAt != 0 {
114		return runtime.ContainerState_CONTAINER_CREATED
115	}
116	return runtime.ContainerState_CONTAINER_UNKNOWN
117}
118
119// encode encodes Status into bytes in json format.
120func (s *Status) encode() ([]byte, error) {
121	return json.Marshal(&versionedStatus{
122		Version: statusVersion,
123		Status:  *s,
124	})
125}
126
127// decode decodes Status from bytes.
128func (s *Status) decode(data []byte) error {
129	versioned := &versionedStatus{}
130	if err := json.Unmarshal(data, versioned); err != nil {
131		return err
132	}
133	// Handle old version after upgrade.
134	switch versioned.Version {
135	case statusVersion:
136		*s = versioned.Status
137		return nil
138	}
139	return errors.New("unsupported version")
140}
141
142// UpdateFunc is function used to update the container status. If there
143// is an error, the update will be rolled back.
144type UpdateFunc func(Status) (Status, error)
145
146// StatusStorage manages the container status with a storage backend.
147type StatusStorage interface {
148	// Get a container status.
149	Get() Status
150	// UpdateSync updates the container status and the on disk checkpoint.
151	// Note that the update MUST be applied in one transaction.
152	UpdateSync(UpdateFunc) error
153	// Update the container status. Note that the update MUST be applied
154	// in one transaction.
155	Update(UpdateFunc) error
156	// Delete the container status.
157	// Note:
158	// * Delete should be idempotent.
159	// * The status must be deleted in one transaction.
160	Delete() error
161}
162
163// StoreStatus creates the storage containing the passed in container status with the
164// specified id.
165// The status MUST be created in one transaction.
166func StoreStatus(root, id string, status Status) (StatusStorage, error) {
167	data, err := status.encode()
168	if err != nil {
169		return nil, errors.Wrap(err, "failed to encode status")
170	}
171	path := filepath.Join(root, "status")
172	if err := continuity.AtomicWriteFile(path, data, 0600); err != nil {
173		return nil, errors.Wrapf(err, "failed to checkpoint status to %q", path)
174	}
175	return &statusStorage{
176		path:   path,
177		status: status,
178	}, nil
179}
180
181// LoadStatus loads container status from checkpoint. There shouldn't be threads
182// writing to the file during loading.
183func LoadStatus(root, id string) (Status, error) {
184	path := filepath.Join(root, "status")
185	data, err := ioutil.ReadFile(path)
186	if err != nil {
187		return Status{}, errors.Wrapf(err, "failed to read status from %q", path)
188	}
189	var status Status
190	if err := status.decode(data); err != nil {
191		return Status{}, errors.Wrapf(err, "failed to decode status %q", data)
192	}
193	return status, nil
194}
195
196type statusStorage struct {
197	sync.RWMutex
198	path   string
199	status Status
200}
201
202// Get a copy of container status.
203func (s *statusStorage) Get() Status {
204	s.RLock()
205	defer s.RUnlock()
206	return s.status
207}
208
209// UpdateSync updates the container status and the on disk checkpoint.
210func (s *statusStorage) UpdateSync(u UpdateFunc) error {
211	s.Lock()
212	defer s.Unlock()
213	newStatus, err := u(s.status)
214	if err != nil {
215		return err
216	}
217	data, err := newStatus.encode()
218	if err != nil {
219		return errors.Wrap(err, "failed to encode status")
220	}
221	if err := continuity.AtomicWriteFile(s.path, data, 0600); err != nil {
222		return errors.Wrapf(err, "failed to checkpoint status to %q", s.path)
223	}
224	s.status = newStatus
225	return nil
226}
227
228// Update the container status.
229func (s *statusStorage) Update(u UpdateFunc) error {
230	s.Lock()
231	defer s.Unlock()
232	newStatus, err := u(s.status)
233	if err != nil {
234		return err
235	}
236	s.status = newStatus
237	return nil
238}
239
240// Delete deletes the container status from disk atomically.
241func (s *statusStorage) Delete() error {
242	temp := filepath.Dir(s.path) + ".del-" + filepath.Base(s.path)
243	if err := os.Rename(s.path, temp); err != nil && !os.IsNotExist(err) {
244		return err
245	}
246	return os.RemoveAll(temp)
247}
248