github.com/xeptore/docker-cli@v20.10.14+incompatible/cli/context/store/store.go (about) 1 package store 2 3 import ( 4 "archive/tar" 5 "archive/zip" 6 "bufio" 7 "bytes" 8 _ "crypto/sha256" // ensure ids can be computed 9 "encoding/json" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "net/http" 14 "path" 15 "path/filepath" 16 "regexp" 17 "strings" 18 19 "github.com/docker/docker/errdefs" 20 digest "github.com/opencontainers/go-digest" 21 "github.com/pkg/errors" 22 ) 23 24 const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$" 25 26 var restrictedNameRegEx = regexp.MustCompile(restrictedNamePattern) 27 28 // Store provides a context store for easily remembering endpoints configuration 29 type Store interface { 30 Reader 31 Lister 32 Writer 33 StorageInfoProvider 34 } 35 36 // Reader provides read-only (without list) access to context data 37 type Reader interface { 38 GetMetadata(name string) (Metadata, error) 39 ListTLSFiles(name string) (map[string]EndpointFiles, error) 40 GetTLSData(contextName, endpointName, fileName string) ([]byte, error) 41 } 42 43 // Lister provides listing of contexts 44 type Lister interface { 45 List() ([]Metadata, error) 46 } 47 48 // ReaderLister combines Reader and Lister interfaces 49 type ReaderLister interface { 50 Reader 51 Lister 52 } 53 54 // StorageInfoProvider provides more information about storage details of contexts 55 type StorageInfoProvider interface { 56 GetStorageInfo(contextName string) StorageInfo 57 } 58 59 // Writer provides write access to context data 60 type Writer interface { 61 CreateOrUpdate(meta Metadata) error 62 Remove(name string) error 63 ResetTLSMaterial(name string, data *ContextTLSData) error 64 ResetEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error 65 } 66 67 // ReaderWriter combines Reader and Writer interfaces 68 type ReaderWriter interface { 69 Reader 70 Writer 71 } 72 73 // Metadata contains metadata about a context and its endpoints 74 type Metadata struct { 75 Name string `json:",omitempty"` 76 Metadata interface{} `json:",omitempty"` 77 Endpoints map[string]interface{} `json:",omitempty"` 78 } 79 80 // StorageInfo contains data about where a given context is stored 81 type StorageInfo struct { 82 MetadataPath string 83 TLSPath string 84 } 85 86 // EndpointTLSData represents tls data for a given endpoint 87 type EndpointTLSData struct { 88 Files map[string][]byte 89 } 90 91 // ContextTLSData represents tls data for a whole context 92 type ContextTLSData struct { 93 Endpoints map[string]EndpointTLSData 94 } 95 96 // New creates a store from a given directory. 97 // If the directory does not exist or is empty, initialize it 98 func New(dir string, cfg Config) Store { 99 metaRoot := filepath.Join(dir, metadataDir) 100 tlsRoot := filepath.Join(dir, tlsDir) 101 102 return &store{ 103 meta: &metadataStore{ 104 root: metaRoot, 105 config: cfg, 106 }, 107 tls: &tlsStore{ 108 root: tlsRoot, 109 }, 110 } 111 } 112 113 type store struct { 114 meta *metadataStore 115 tls *tlsStore 116 } 117 118 func (s *store) List() ([]Metadata, error) { 119 return s.meta.list() 120 } 121 122 func (s *store) CreateOrUpdate(meta Metadata) error { 123 return s.meta.createOrUpdate(meta) 124 } 125 126 func (s *store) Remove(name string) error { 127 id := contextdirOf(name) 128 if err := s.meta.remove(id); err != nil { 129 return patchErrContextName(err, name) 130 } 131 return patchErrContextName(s.tls.removeAllContextData(id), name) 132 } 133 134 func (s *store) GetMetadata(name string) (Metadata, error) { 135 res, err := s.meta.get(contextdirOf(name)) 136 patchErrContextName(err, name) 137 return res, err 138 } 139 140 func (s *store) ResetTLSMaterial(name string, data *ContextTLSData) error { 141 id := contextdirOf(name) 142 if err := s.tls.removeAllContextData(id); err != nil { 143 return patchErrContextName(err, name) 144 } 145 if data == nil { 146 return nil 147 } 148 for ep, files := range data.Endpoints { 149 for fileName, data := range files.Files { 150 if err := s.tls.createOrUpdate(id, ep, fileName, data); err != nil { 151 return patchErrContextName(err, name) 152 } 153 } 154 } 155 return nil 156 } 157 158 func (s *store) ResetEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error { 159 id := contextdirOf(contextName) 160 if err := s.tls.removeAllEndpointData(id, endpointName); err != nil { 161 return patchErrContextName(err, contextName) 162 } 163 if data == nil { 164 return nil 165 } 166 for fileName, data := range data.Files { 167 if err := s.tls.createOrUpdate(id, endpointName, fileName, data); err != nil { 168 return patchErrContextName(err, contextName) 169 } 170 } 171 return nil 172 } 173 174 func (s *store) ListTLSFiles(name string) (map[string]EndpointFiles, error) { 175 res, err := s.tls.listContextData(contextdirOf(name)) 176 return res, patchErrContextName(err, name) 177 } 178 179 func (s *store) GetTLSData(contextName, endpointName, fileName string) ([]byte, error) { 180 res, err := s.tls.getData(contextdirOf(contextName), endpointName, fileName) 181 return res, patchErrContextName(err, contextName) 182 } 183 184 func (s *store) GetStorageInfo(contextName string) StorageInfo { 185 dir := contextdirOf(contextName) 186 return StorageInfo{ 187 MetadataPath: s.meta.contextDir(dir), 188 TLSPath: s.tls.contextDir(dir), 189 } 190 } 191 192 // ValidateContextName checks a context name is valid. 193 func ValidateContextName(name string) error { 194 if name == "" { 195 return errors.New("context name cannot be empty") 196 } 197 if name == "default" { 198 return errors.New(`"default" is a reserved context name`) 199 } 200 if !restrictedNameRegEx.MatchString(name) { 201 return fmt.Errorf("context name %q is invalid, names are validated against regexp %q", name, restrictedNamePattern) 202 } 203 return nil 204 } 205 206 // Export exports an existing namespace into an opaque data stream 207 // This stream is actually a tarball containing context metadata and TLS materials, but it does 208 // not map 1:1 the layout of the context store (don't try to restore it manually without calling store.Import) 209 func Export(name string, s Reader) io.ReadCloser { 210 reader, writer := io.Pipe() 211 go func() { 212 tw := tar.NewWriter(writer) 213 defer tw.Close() 214 defer writer.Close() 215 meta, err := s.GetMetadata(name) 216 if err != nil { 217 writer.CloseWithError(err) 218 return 219 } 220 metaBytes, err := json.Marshal(&meta) 221 if err != nil { 222 writer.CloseWithError(err) 223 return 224 } 225 if err = tw.WriteHeader(&tar.Header{ 226 Name: metaFile, 227 Mode: 0644, 228 Size: int64(len(metaBytes)), 229 }); err != nil { 230 writer.CloseWithError(err) 231 return 232 } 233 if _, err = tw.Write(metaBytes); err != nil { 234 writer.CloseWithError(err) 235 return 236 } 237 tlsFiles, err := s.ListTLSFiles(name) 238 if err != nil { 239 writer.CloseWithError(err) 240 return 241 } 242 if err = tw.WriteHeader(&tar.Header{ 243 Name: "tls", 244 Mode: 0700, 245 Size: 0, 246 Typeflag: tar.TypeDir, 247 }); err != nil { 248 writer.CloseWithError(err) 249 return 250 } 251 for endpointName, endpointFiles := range tlsFiles { 252 if err = tw.WriteHeader(&tar.Header{ 253 Name: path.Join("tls", endpointName), 254 Mode: 0700, 255 Size: 0, 256 Typeflag: tar.TypeDir, 257 }); err != nil { 258 writer.CloseWithError(err) 259 return 260 } 261 for _, fileName := range endpointFiles { 262 data, err := s.GetTLSData(name, endpointName, fileName) 263 if err != nil { 264 writer.CloseWithError(err) 265 return 266 } 267 if err = tw.WriteHeader(&tar.Header{ 268 Name: path.Join("tls", endpointName, fileName), 269 Mode: 0600, 270 Size: int64(len(data)), 271 }); err != nil { 272 writer.CloseWithError(err) 273 return 274 } 275 if _, err = tw.Write(data); err != nil { 276 writer.CloseWithError(err) 277 return 278 } 279 } 280 } 281 }() 282 return reader 283 } 284 285 const ( 286 maxAllowedFileSizeToImport int64 = 10 << 20 287 zipType string = "application/zip" 288 ) 289 290 func getImportContentType(r *bufio.Reader) (string, error) { 291 head, err := r.Peek(512) 292 if err != nil && err != io.EOF { 293 return "", err 294 } 295 296 return http.DetectContentType(head), nil 297 } 298 299 // Import imports an exported context into a store 300 func Import(name string, s Writer, reader io.Reader) error { 301 // Buffered reader will not advance the buffer, needed to determine content type 302 r := bufio.NewReader(reader) 303 304 importContentType, err := getImportContentType(r) 305 if err != nil { 306 return err 307 } 308 switch importContentType { 309 case zipType: 310 return importZip(name, s, r) 311 default: 312 // Assume it's a TAR (TAR does not have a "magic number") 313 return importTar(name, s, r) 314 } 315 } 316 317 func isValidFilePath(p string) error { 318 if p != metaFile && !strings.HasPrefix(p, "tls/") { 319 return errors.New("unexpected context file") 320 } 321 if path.Clean(p) != p { 322 return errors.New("unexpected path format") 323 } 324 if strings.Contains(p, `\`) { 325 return errors.New(`unexpected '\' in path`) 326 } 327 return nil 328 } 329 330 func importTar(name string, s Writer, reader io.Reader) error { 331 tr := tar.NewReader(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport}) 332 tlsData := ContextTLSData{ 333 Endpoints: map[string]EndpointTLSData{}, 334 } 335 var importedMetaFile bool 336 for { 337 hdr, err := tr.Next() 338 if err == io.EOF { 339 break 340 } 341 if err != nil { 342 return err 343 } 344 if hdr.Typeflag != tar.TypeReg { 345 // skip this entry, only taking files into account 346 continue 347 } 348 if err := isValidFilePath(hdr.Name); err != nil { 349 return errors.Wrap(err, hdr.Name) 350 } 351 if hdr.Name == metaFile { 352 data, err := ioutil.ReadAll(tr) 353 if err != nil { 354 return err 355 } 356 meta, err := parseMetadata(data, name) 357 if err != nil { 358 return err 359 } 360 if err := s.CreateOrUpdate(meta); err != nil { 361 return err 362 } 363 importedMetaFile = true 364 } else if strings.HasPrefix(hdr.Name, "tls/") { 365 data, err := ioutil.ReadAll(tr) 366 if err != nil { 367 return err 368 } 369 if err := importEndpointTLS(&tlsData, hdr.Name, data); err != nil { 370 return err 371 } 372 } 373 } 374 if !importedMetaFile { 375 return errdefs.InvalidParameter(errors.New("invalid context: no metadata found")) 376 } 377 return s.ResetTLSMaterial(name, &tlsData) 378 } 379 380 func importZip(name string, s Writer, reader io.Reader) error { 381 body, err := ioutil.ReadAll(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport}) 382 if err != nil { 383 return err 384 } 385 zr, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) 386 if err != nil { 387 return err 388 } 389 tlsData := ContextTLSData{ 390 Endpoints: map[string]EndpointTLSData{}, 391 } 392 393 var importedMetaFile bool 394 for _, zf := range zr.File { 395 fi := zf.FileInfo() 396 if !fi.Mode().IsRegular() { 397 // skip this entry, only taking regular files into account 398 continue 399 } 400 if err := isValidFilePath(zf.Name); err != nil { 401 return errors.Wrap(err, zf.Name) 402 } 403 if zf.Name == metaFile { 404 f, err := zf.Open() 405 if err != nil { 406 return err 407 } 408 409 data, err := ioutil.ReadAll(&LimitedReader{R: f, N: maxAllowedFileSizeToImport}) 410 defer f.Close() 411 if err != nil { 412 return err 413 } 414 meta, err := parseMetadata(data, name) 415 if err != nil { 416 return err 417 } 418 if err := s.CreateOrUpdate(meta); err != nil { 419 return err 420 } 421 importedMetaFile = true 422 } else if strings.HasPrefix(zf.Name, "tls/") { 423 f, err := zf.Open() 424 if err != nil { 425 return err 426 } 427 data, err := ioutil.ReadAll(f) 428 defer f.Close() 429 if err != nil { 430 return err 431 } 432 err = importEndpointTLS(&tlsData, zf.Name, data) 433 if err != nil { 434 return err 435 } 436 } 437 } 438 if !importedMetaFile { 439 return errdefs.InvalidParameter(errors.New("invalid context: no metadata found")) 440 } 441 return s.ResetTLSMaterial(name, &tlsData) 442 } 443 444 func parseMetadata(data []byte, name string) (Metadata, error) { 445 var meta Metadata 446 if err := json.Unmarshal(data, &meta); err != nil { 447 return meta, err 448 } 449 if err := ValidateContextName(name); err != nil { 450 return Metadata{}, err 451 } 452 meta.Name = name 453 return meta, nil 454 } 455 456 func importEndpointTLS(tlsData *ContextTLSData, path string, data []byte) error { 457 parts := strings.SplitN(strings.TrimPrefix(path, "tls/"), "/", 2) 458 if len(parts) != 2 { 459 // TLS endpoints require archived file directory with 2 layers 460 // i.e. tls/{endpointName}/{fileName} 461 return errors.New("archive format is invalid") 462 } 463 464 epName := parts[0] 465 fileName := parts[1] 466 if _, ok := tlsData.Endpoints[epName]; !ok { 467 tlsData.Endpoints[epName] = EndpointTLSData{ 468 Files: map[string][]byte{}, 469 } 470 } 471 tlsData.Endpoints[epName].Files[fileName] = data 472 return nil 473 } 474 475 type setContextName interface { 476 setContext(name string) 477 } 478 479 type contextDoesNotExistError struct { 480 name string 481 } 482 483 func (e *contextDoesNotExistError) Error() string { 484 return fmt.Sprintf("context %q does not exist", e.name) 485 } 486 487 func (e *contextDoesNotExistError) setContext(name string) { 488 e.name = name 489 } 490 491 // NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound 492 func (e *contextDoesNotExistError) NotFound() {} 493 494 type tlsDataDoesNotExist interface { 495 errdefs.ErrNotFound 496 IsTLSDataDoesNotExist() 497 } 498 499 type tlsDataDoesNotExistError struct { 500 context, endpoint, file string 501 } 502 503 func (e *tlsDataDoesNotExistError) Error() string { 504 return fmt.Sprintf("tls data for %s/%s/%s does not exist", e.context, e.endpoint, e.file) 505 } 506 507 func (e *tlsDataDoesNotExistError) setContext(name string) { 508 e.context = name 509 } 510 511 // NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound 512 func (e *tlsDataDoesNotExistError) NotFound() {} 513 514 // IsTLSDataDoesNotExist satisfies tlsDataDoesNotExist 515 func (e *tlsDataDoesNotExistError) IsTLSDataDoesNotExist() {} 516 517 // IsErrContextDoesNotExist checks if the given error is a "context does not exist" condition 518 func IsErrContextDoesNotExist(err error) bool { 519 _, ok := err.(*contextDoesNotExistError) 520 return ok 521 } 522 523 // IsErrTLSDataDoesNotExist checks if the given error is a "context does not exist" condition 524 func IsErrTLSDataDoesNotExist(err error) bool { 525 _, ok := err.(tlsDataDoesNotExist) 526 return ok 527 } 528 529 type contextdir string 530 531 func contextdirOf(name string) contextdir { 532 return contextdir(digest.FromString(name).Encoded()) 533 } 534 535 func patchErrContextName(err error, name string) error { 536 if typed, ok := err.(setContextName); ok { 537 typed.setContext(name) 538 } 539 return err 540 }