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