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