1package store 2 3import ( 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 24const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$" 25 26var restrictedNameRegEx = regexp.MustCompile(restrictedNamePattern) 27 28// Store provides a context store for easily remembering endpoints configuration 29type Store interface { 30 Reader 31 Lister 32 Writer 33 StorageInfoProvider 34} 35 36// Reader provides read-only (without list) access to context data 37type 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 44type Lister interface { 45 List() ([]Metadata, error) 46} 47 48// ReaderLister combines Reader and Lister interfaces 49type ReaderLister interface { 50 Reader 51 Lister 52} 53 54// StorageInfoProvider provides more information about storage details of contexts 55type StorageInfoProvider interface { 56 GetStorageInfo(contextName string) StorageInfo 57} 58 59// Writer provides write access to context data 60type 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 68type ReaderWriter interface { 69 Reader 70 Writer 71} 72 73// Metadata contains metadata about a context and its endpoints 74type 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 81type StorageInfo struct { 82 MetadataPath string 83 TLSPath string 84} 85 86// EndpointTLSData represents tls data for a given endpoint 87type EndpointTLSData struct { 88 Files map[string][]byte 89} 90 91// ContextTLSData represents tls data for a whole context 92type 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 98func 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 113type store struct { 114 meta *metadataStore 115 tls *tlsStore 116} 117 118func (s *store) List() ([]Metadata, error) { 119 return s.meta.list() 120} 121 122func (s *store) CreateOrUpdate(meta Metadata) error { 123 return s.meta.createOrUpdate(meta) 124} 125 126func (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 134func (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 140func (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 158func (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 174func (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 179func (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 184func (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. 193func 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) 209func 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 285const ( 286 maxAllowedFileSizeToImport int64 = 10 << 20 287 zipType string = "application/zip" 288) 289 290func 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 300func 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 317func 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 330func 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 380func 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 444func 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 456func 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 475type setContextName interface { 476 setContext(name string) 477} 478 479type contextDoesNotExistError struct { 480 name string 481} 482 483func (e *contextDoesNotExistError) Error() string { 484 return fmt.Sprintf("context %q does not exist", e.name) 485} 486 487func (e *contextDoesNotExistError) setContext(name string) { 488 e.name = name 489} 490 491// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound 492func (e *contextDoesNotExistError) NotFound() {} 493 494type tlsDataDoesNotExist interface { 495 errdefs.ErrNotFound 496 IsTLSDataDoesNotExist() 497} 498 499type tlsDataDoesNotExistError struct { 500 context, endpoint, file string 501} 502 503func (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 507func (e *tlsDataDoesNotExistError) setContext(name string) { 508 e.context = name 509} 510 511// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound 512func (e *tlsDataDoesNotExistError) NotFound() {} 513 514// IsTLSDataDoesNotExist satisfies tlsDataDoesNotExist 515func (e *tlsDataDoesNotExistError) IsTLSDataDoesNotExist() {} 516 517// IsErrContextDoesNotExist checks if the given error is a "context does not exist" condition 518func 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 524func IsErrTLSDataDoesNotExist(err error) bool { 525 _, ok := err.(tlsDataDoesNotExist) 526 return ok 527} 528 529type contextdir string 530 531func contextdirOf(name string) contextdir { 532 return contextdir(digest.FromString(name).Encoded()) 533} 534 535func patchErrContextName(err error, name string) error { 536 if typed, ok := err.(setContextName); ok { 537 typed.setContext(name) 538 } 539 return err 540} 541