package operator import ( "bytes" "context" "errors" "fmt" "io" "net/http" "strconv" "mstore/app/descr" "mstore/pkg/auxoci" ) const ( ddmMimeType = "application/vnd.docker.distribution.manifest.v2+json" oimMimeType = "application/vnd.oci.image.manifest.v1+json" ) type ManifestExistsParams struct { Name string Reference string } type ManifestExistsResult struct { ContentLength string ContentType string DockerContentDigest string Exists bool } func (oper *Operator) ManifestExists(ctx context.Context, params *ManifestExistsParams) (*ManifestExistsResult, int, error) { var err error res := &ManifestExistsResult{} if params.Name == "" { err = fmt.Errorf("Empty name") return res, http.StatusBadRequest, err } if params.Reference == "" { err = fmt.Errorf("Empty reference") return res, http.StatusBadRequest, err } oper.logg.Debugf("Head manifest [%s:%s]", params.Name, params.Reference) var manifest descr.Manifest var exists bool if stringLikeSHA256Digest(params.Reference) { digest := fmt.Sprintf("%s:%s", sha256prefix, params.Reference) oper.logg.Debugf("Find manifest %s by digest %s", params.Name, params.Reference) exists, manifest, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digest) if err != nil { return res, http.StatusInternalServerError, err } if !exists { return res, http.StatusNotFound, err } } else { oper.logg.Debugf("Find manifest %s by reference %s", params.Name, params.Reference) exists, manifest, err = oper.mdb.GetManifestByReference(ctx, params.Name, params.Reference) if err != nil { return res, http.StatusInternalServerError, err } if !exists { return res, http.StatusNotFound, err } } digest := auxoci.SHA256DigestFromString(manifest.Payload) payloadSize := len(manifest.Payload) res.ContentLength = strconv.FormatInt(int64(payloadSize), 10) res.ContentType = manifest.ContentType res.DockerContentDigest = digest.String() res.Exists = exists return res, http.StatusOK, err } type PutManifestParams struct { ContentType string ContentLength string Name string Reference string Reader io.Reader } type PutManifestResult struct { Location string } // TODO: lock for the name-reference or simular? func (oper *Operator) PutManifest(ctx context.Context, params *PutManifestParams) (*PutManifestResult, int, error) { var err error res := &PutManifestResult{} //oper.logg.Debugf("Put manifest %s:%s", params.Name, params.Reference) if params.Reference == "" { err = fmt.Errorf("Empty reference") return res, http.StatusBadRequest, err } if params.Name == "" { err = fmt.Errorf("Empty name") return res, http.StatusBadRequest, err } // Check Content-Type if params.ContentType != oimMimeType { err = fmt.Errorf("Unknown or empty Content-Type: %s", params.ContentType) return res, http.StatusNotFound, err } if params.ContentLength == "" { code := http.StatusLengthRequired err = fmt.Errorf("Content-Length is empty") return res, code, err } contentLength, err := strconv.ParseInt(params.ContentLength, 10, 64) if err != nil { err = fmt.Errorf("Cannot parse Content-Length value [%s]: %v", params.ContentLength, err) code := http.StatusLengthRequired return res, code, err } // Copy manifest data buffer := bytes.NewBuffer(nil) _, err = io.Copy(buffer, params.Reader) if err != nil { return res, http.StatusInternalServerError, err } incomingManifestBytes := buffer.Bytes() if int64(len(incomingManifestBytes)) != contentLength { err = fmt.Errorf("Mismatch Content-Length and received manifest size: %d vs %d", contentLength, len(incomingManifestBytes)) code := http.StatusInternalServerError return res, code, err } if len(incomingManifestBytes) > (4 * 1024 * 1024) { err = fmt.Errorf("Payload more 4M: %d bytes", len(incomingManifestBytes)) code := http.StatusRequestEntityTooLarge return res, code, err } //oper.logg.Debugf("Manifest data: [%s]", string(incomingManifestBytes)) incomingManifest, err := auxoci.ParseOCIManifest(incomingManifestBytes) if err != nil { err = fmt.Errorf("Parsing OCI manifest error: %v", err) return res, http.StatusInternalServerError, err } if incomingManifest.MediaType == "" { incomingManifest.MediaType = params.ContentType } manifestExists, existengManifestDescr, err := oper.mdb.GetManifestByReference(ctx, params.Name, params.Reference) if err != nil { return res, http.StatusInternalServerError, err } name := params.Name reference := params.Reference incomingManifestDescr, incomingLayerDescrs, err := descrsFromManifest(name, reference, incomingManifest, incomingManifestBytes) // Always check layer files for availability var blobError error for _, layer := range incomingLayerDescrs { layerExists, _, err := oper.store.BlobExists(layer.Digest) if err != nil { return res, http.StatusInternalServerError, err } if !layerExists { err := fmt.Errorf("Layer %s not found.", layer.Digest) blobError = errors.Join(blobError, err) } } if blobError != nil { return res, http.StatusInternalServerError, blobError } if !manifestExists { // Store manifest and layesrs data err = oper.mdb.InsertManifestWithLayers(ctx, &incomingManifestDescr, incomingLayerDescrs) if err != nil { return res, http.StatusInternalServerError, err } } else { /* TODO: only update descr if bytes.Equal(existingManifestBytes, incomingManifestBytes) { return res, http.StatusCreated, err } */ existingManifestBytes := []byte(existengManifestDescr.Payload) existingManifest, err := auxoci.ParseOCIManifest(existingManifestBytes) if err != nil { return res, http.StatusInternalServerError, err } addedBlobDescrs, uselessBlobDescrs, err := layersDiff(name, reference, existingManifest, incomingManifest, incomingManifestBytes) if err != nil { return res, http.StatusInternalServerError, err } // Starting manifest and blobs transaction err = oper.mdb.UpdateManifestWithBlobs(ctx, &incomingManifestDescr, addedBlobDescrs, uselessBlobDescrs) if err != nil { return res, http.StatusInternalServerError, err } for _, blob := range uselessBlobDescrs { exists, _, err := oper.store.BlobExists(blob.Digest) if err != nil { return res, http.StatusInternalServerError, err } blobUsage, err := oper.mdb.GetBlobUsage(ctx, blob.Digest) if err != nil { return res, http.StatusInternalServerError, err } if exists && blobUsage == 0 { oper.logg.Debugf("Delete file %s:%s blob %s", name, reference, blob.Digest) err = oper.store.DeleteBlob(blob.Digest) if err != nil { return res, http.StatusInternalServerError, err } } } } res.Location = fmt.Sprintf(`/v2/%s/manifests/%s`, params.Name, params.Reference) return res, http.StatusCreated, err } type GetManifestParams struct { Name string Reference string } type GetManifestResult struct { ContentLength string ContentType string DockerContentDigest string Payload string } func (oper *Operator) GetManifest(ctx context.Context, params *GetManifestParams) (*GetManifestResult, int, error) { var err error res := &GetManifestResult{} //oper.logg.Debugf("Get manifest %s:%s", params.Name, params.Reference) if params.Name == "" { err = fmt.Errorf("Empty name") return res, http.StatusBadRequest, err } if params.Reference == "" { err = fmt.Errorf("Empty reference") return res, http.StatusBadRequest, err } manifestDescr := descr.Manifest{} var exists bool if stringLikeSHADigest(params.Reference) { digest := normalizeSHADigest(params.Reference) oper.logg.Debugf("Get manifest %s with digest %s", params.Name, params.Reference) exists, manifestDescr, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digest) if err != nil { return res, http.StatusInternalServerError, err } if !exists { return res, http.StatusNotFound, err } } else { oper.logg.Debugf("Get manifest %s with tag %s", params.Name, params.Reference) exists, manifestDescr, err = oper.mdb.GetManifestByReference(ctx, params.Name, params.Reference) if err != nil { return res, http.StatusInternalServerError, err } if !exists { return res, http.StatusNotFound, err } } manifestDigest := auxoci.SHA256DigestFromString(manifestDescr.Payload) res.DockerContentDigest = manifestDigest.String() res.ContentLength = strconv.FormatInt(int64(len(manifestDescr.Payload)), 10) res.ContentType = manifestDescr.ContentType res.Payload = manifestDescr.Payload return res, http.StatusOK, err }