/
sandbox.go
356 lines (299 loc) · 8.93 KB
/
sandbox.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
package lxf
import (
"crypto/md5" // nolint: gosec
"fmt"
"strconv"
"strings"
"time"
"github.com/automaticserver/lxe/lxf/device"
"github.com/automaticserver/lxe/network/cloudinit"
"github.com/automaticserver/lxe/shared"
"github.com/ghodss/yaml"
"github.com/lxc/lxd/shared/api"
"github.com/lxc/lxd/shared/logger"
"k8s.io/apimachinery/pkg/util/uuid"
)
const (
// Default device name of the root disk when initializing lxd
lxdInitDefaultDiskName = "root"
// Default device name of the nic interface when initializing lxd
lxdInitDefaultNicName = "eth0"
cfgHostname = "user.host_name"
cfgLogDirectory = "user.log_directory"
cfgCreatedAt = "user.created_at"
cfgNetworkConfig = "user.networkconfig"
cfgNetworkConfigNameservers = cfgNetworkConfig + ".nameservers"
cfgNetworkConfigSearches = cfgNetworkConfig + ".searches"
cfgNetworkConfigMode = cfgNetworkConfig + ".mode"
cfgNetworkConfigModeData = cfgNetworkConfig + ".modedata"
cfgCloudInitNetworkConfig = "user.network-config" // write-only field
cfgCloudInitVendorData = "user.vendor-data" // write-only field
)
var (
sandboxConfigStore = NewConfigStore().WithReserved(
append([]string{
cfgLogDirectory,
cfgState,
cfgHostname,
cfgCloudInitNetworkConfig,
cfgCloudInitVendorData,
}, reservedConfigCRI...,
)...,
).WithReservedPrefixes(
append([]string{
cfgNetworkConfig,
}, reservedConfigPrefixesCRI...,
)...,
)
)
// Sandbox is an abstraction of a CRI PodSandbox saved as a LXD profile
type Sandbox struct {
// LXDObject inherits common CRI fields
LXDObject
// UsedBy contains the names of the containers using this profile
// It is read only.
UsedBy []string
// CRIObject inherits common CRI fields
CRIObject
// Metadata contains user defined data
Metadata SandboxMetadata
// Hostname to be set for containers if defined
Hostname string
// NetworkConfig to be applied for the sandbox and it's containers
NetworkConfig NetworkConfig
// State contains the current state of this sandbox
State SandboxState
// LogDirectory TODO, to be implemented?
LogDirectory string
// CloudInitNetworkConfigEntries to set
CloudInitNetworkConfigEntries []cloudinit.NetworkConfigEntryPhysical
// sandbox is the parent sandbox of this container
containers []*Container
}
// SandboxState defines the state of the sandbox
type SandboxState string
// These are valid sandbox statuses. SandboxReady means a resource is in the condition.
// SandboxNotReady means a resource is not in the condition.
const (
SandboxNotReady SandboxState = "notready"
SandboxReady SandboxState = "ready"
)
// SandboxMetadata contains common metadata values
type SandboxMetadata struct {
Attempt uint32
Name string
Namespace string
UID string
}
// NetworkConfig contains the network config
// searches and nameservers must not be empty to be valid
type NetworkConfig struct {
Nameservers []string
Searches []string
// Mode describes the type of networking
Mode NetworkMode
// ModeData allows Mode-specific data to be persisted
ModeData map[string]string
}
// NetworkMode defines the type of the container network
type NetworkMode string
// These are valid network modes. NetworkHost means the container to share the host's network namespace
// NetworkCNI means the CNI handles the interface, NetworkBridged means the container gets a interface
// from a predefined bridge, NetworkNone is used when the requested mode can't be used
const (
NetworkHost NetworkMode = "node"
NetworkCNI NetworkMode = "cni"
NetworkBridged NetworkMode = "bridged"
NetworkNone NetworkMode = "none"
)
func (s NetworkMode) String() string {
return string(s)
}
func getNetworkMode(str string) NetworkMode {
for _, v := range []NetworkMode{NetworkHost, NetworkCNI, NetworkBridged, NetworkNone} {
if str == string(v) {
return v
}
}
return NetworkNone
}
func (s SandboxState) String() string {
return string(s)
}
func getSandboxState(str string) SandboxState {
if str == string(SandboxNotReady) {
return SandboxNotReady
}
return SandboxReady
}
// Containers looks up all assigned containers
// Implemented as lazy loading, and returns same result if already looked up
// Not thread safe! But it's expected the pointers stay in the same routine
func (s *Sandbox) Containers() ([]*Container, error) {
var err error
if s.containers == nil {
s.containers, err = s.getContainers()
if err != nil {
return nil, err
}
}
return s.containers, nil
}
func (s *Sandbox) getContainers() ([]*Container, error) {
cl := []*Container{}
for _, cntName := range s.UsedBy {
c, err := s.client.GetContainer(cntName)
if err != nil {
return nil, err
}
cl = append(cl, c)
}
return cl, nil
}
// refresh loads the profile again from LXD to obtain new ETag
// Will not load new data!
func (s *Sandbox) refresh() error {
r, err := s.client.GetSandbox(s.ID)
if err != nil {
return err
}
s.ETag = r.ETag
return nil
}
// Apply will save the changes of a sandbox
func (s *Sandbox) Apply() error {
// A new sandbox gets also some default values
// except ID, which is generated inline in unexported method apply()
if s.ID == "" {
s.State = SandboxReady
s.CreatedAt = time.Now()
}
// Always stop inheriting default eth0 device
s.Devices.Upsert(&device.None{
KeyName: lxdInitDefaultNicName,
})
err := s.apply()
if err != nil {
return err
}
return s.refresh()
}
// Stop set the sandbox state to SandboxNotReady
func (s *Sandbox) Stop() error {
s.State = SandboxNotReady
return s.apply()
}
// Delete will delete the given sandbox, returns nil when sandbox is already deleted
func (s *Sandbox) Delete() error {
err := s.client.server.DeleteProfile(s.ID)
if err != nil {
if shared.IsErrNotFound(err) {
return nil
}
return err
}
return nil
}
// apply saves the changes to LXD
func (s *Sandbox) apply() error {
config := map[string]string{
cfgState: s.State.String(),
cfgIsCRI: strconv.FormatBool(true),
cfgCreatedAt: strconv.FormatInt(s.CreatedAt.UnixNano(), 10),
cfgMetaAttempt: strconv.FormatUint(uint64(s.Metadata.Attempt), 10),
cfgMetaName: s.Metadata.Name,
cfgMetaNamespace: s.Metadata.Namespace,
cfgMetaUID: s.Metadata.UID,
cfgHostname: s.Hostname,
cfgLogDirectory: s.LogDirectory,
cfgNetworkConfigNameservers: strings.Join(s.NetworkConfig.Nameservers, ","),
cfgNetworkConfigSearches: strings.Join(s.NetworkConfig.Searches, ","),
cfgNetworkConfigMode: s.NetworkConfig.Mode.String(),
}
// write NetworkConfigData as yaml
yml, err := yaml.Marshal(s.NetworkConfig.ModeData)
if err != nil {
return err
}
config[cfgNetworkConfigModeData] = string(yml)
// write labels
for key, val := range s.Labels {
config[cfgLabels+"."+key] = val
}
// and annotations
for key, val := range s.Annotations {
config[cfgAnnotations+"."+key] = val
}
// and config keys
for key, val := range s.Config {
if sandboxConfigStore.IsReserved(key) {
logger.Warnf("config key '%v' is reserved and can not be used", key)
} else {
config[key] = val
}
}
// write cloud-init network config
data := cloudinit.NetworkConfig{
Version: 1,
Config: []interface{}{},
}
if len(s.NetworkConfig.Nameservers) > 0 &&
len(s.NetworkConfig.Searches) > 0 {
data.Config = append(data.Config, cloudinit.NetworkConfigEntryNameserver{
NetworkConfigEntry: cloudinit.NetworkConfigEntry{
Type: "nameserver",
},
Address: s.NetworkConfig.Nameservers,
Search: s.NetworkConfig.Searches,
})
}
for _, entry := range s.CloudInitNetworkConfigEntries {
data.Config = append(data.Config, entry)
}
yml, err = yaml.Marshal(data)
if err != nil {
return err
}
config[cfgCloudInitNetworkConfig] = string(yml)
// write cloud-init vendor data if we have hostname and search
if s.Hostname != "" {
config[cfgCloudInitVendorData] = fmt.Sprintf(`#cloud-config
hostname: %s
manage_etc_hosts: true
`, s.Hostname)
}
devices := make(map[string]map[string]string)
for _, d := range s.Devices {
name, options := d.ToMap()
devices[name] = options
}
config[cfgSchema] = SchemaVersionProfile
profile := api.ProfilePut{
Config: config,
Devices: devices,
}
if s.ID == "" { // profile has to be created
s.ID = s.CreateID()
return s.client.server.CreateProfile(api.ProfilesPost{
Name: s.ID,
ProfilePut: profile,
})
}
// else profile has to be updated
if s.ETag == "" {
return fmt.Errorf("update profile not allowed: %w", ErrMissingETag)
}
err = s.client.server.UpdateProfile(s.ID, profile, s.ETag)
if err != nil {
if shared.IsErrNotFound(err) {
return fmt.Errorf("sandbox %w: %s", shared.NewErrNotFound(), s.ID)
}
return err
}
return nil
}
// CreateID creates a unique profile id
func (s *Sandbox) CreateID() string {
bin := md5.Sum([]byte(uuid.NewUUID())) // nolint: gosec
return string(s.Metadata.Name[0]) + b32lowerEncoder.EncodeToString(bin[:])[:15]
}