119 lines
5.0 KiB
Go
119 lines
5.0 KiB
Go
// Package api — работа с Fission storagesvc (S3-совместимое хранилище архивов).
|
||
//
|
||
// Этот файл содержит низкоуровневые операции загрузки и удаления архивов через
|
||
// HTTP API storagesvc. Storagesvc хранит zip-архивы функций в S3 (bucket sless-functions).
|
||
// Package CRD ссылается на архив через spec.deployment.url (type: url).
|
||
//
|
||
// Используется из function_code.go и function_archive.go для сохранения/замены кода функций.
|
||
package api
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// deleteFromStoragesvc удаляет архив из S3 через storagesvc по URL из Package spec.
|
||
// URL имеет формат: http://storagesvc.../v1/archive?id=fission/UUID
|
||
// Best-effort: ошибка логируется, но не прерывает операцию удаления.
|
||
func (s *Server) deleteFromStoragesvc(ctx context.Context, archiveURL string) {
|
||
if s.storagesvcURL == "" || archiveURL == "" {
|
||
return
|
||
}
|
||
// archiveURL = "http://storagesvc.../v1/archive?id=fission/UUID"
|
||
// Строим DELETE URL к storagesvc, сохраняя query-параметр id
|
||
parsed, err := url.Parse(archiveURL)
|
||
if err != nil {
|
||
log.Printf("deleteFromStoragesvc: parse url %q: %v", archiveURL, err)
|
||
return
|
||
}
|
||
deleteURL := strings.TrimRight(s.storagesvcURL, "/") + "/v1/archive?" + parsed.RawQuery
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, deleteURL, nil)
|
||
if err != nil {
|
||
log.Printf("deleteFromStoragesvc: build request: %v", err)
|
||
return
|
||
}
|
||
resp, err := s.http.Do(req)
|
||
if err != nil {
|
||
log.Printf("deleteFromStoragesvc: %v", err)
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||
body, _ := io.ReadAll(resp.Body)
|
||
log.Printf("deleteFromStoragesvc: status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||
return
|
||
}
|
||
log.Printf("deleteFromStoragesvc: deleted %s", parsed.Query().Get("id"))
|
||
}
|
||
|
||
// uploadToStoragesvc загружает байты в Fission storagesvc и возвращает URL для package archive.
|
||
// Если storagesvcURL не задан — возвращает пустую строку (fallback на literal).
|
||
func (s *Server) uploadToStoragesvc(ctx context.Context, data []byte) (string, error) {
|
||
if s.storagesvcURL == "" {
|
||
log.Printf("uploadToStoragesvc: storagesvcURL is empty, skip upload")
|
||
return "", nil
|
||
}
|
||
log.Printf("uploadToStoragesvc: uploading %d bytes to %s", len(data), s.storagesvcURL)
|
||
uploadURL := strings.TrimRight(s.storagesvcURL, "/") + "/v1/archive"
|
||
body := &bytes.Reader{}
|
||
// multipart/form-data с полем uploadfile
|
||
var buf bytes.Buffer
|
||
boundary := fmt.Sprintf("fission%d", time.Now().UnixNano())
|
||
buf.WriteString("--" + boundary + "\r\n")
|
||
buf.WriteString(fmt.Sprintf("Content-Disposition: form-data; name=\"uploadfile\"; filename=\"archive.zip\"\r\n"))
|
||
buf.WriteString("Content-Type: application/octet-stream\r\n\r\n")
|
||
buf.Write(data)
|
||
buf.WriteString("\r\n--" + boundary + "--\r\n")
|
||
body = bytes.NewReader(buf.Bytes())
|
||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL, body)
|
||
if err != nil {
|
||
return "", fmt.Errorf("build storagesvc upload request: %w", err)
|
||
}
|
||
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
|
||
req.Header.Set("X-File-Size", fmt.Sprintf("%d", len(data)))
|
||
|
||
resp, err := s.http.Do(req)
|
||
if err != nil {
|
||
return "", fmt.Errorf("storagesvc upload: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
respBody, _ := io.ReadAll(resp.Body)
|
||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||
return "", fmt.Errorf("storagesvc upload status %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
|
||
}
|
||
var result struct {
|
||
ID string `json:"id"`
|
||
}
|
||
if err := json.Unmarshal(respBody, &result); err != nil || result.ID == "" {
|
||
return "", fmt.Errorf("storagesvc upload: bad response: %s", string(respBody))
|
||
}
|
||
archiveURL := strings.TrimRight(s.storagesvcURL, "/") + "/v1/archive?id=" + result.ID
|
||
return archiveURL, nil
|
||
}
|
||
|
||
// buildDeploySpec строит spec.deployment для Fission Package.
|
||
// Если storagesvcURL задан — загружает архив в S3 через storagesvc и возвращает type: url.
|
||
// Иначе — возвращает type: literal с base64-кодом.
|
||
func (s *Server) buildDeploySpec(ctx context.Context, data []byte) (map[string]any, error) {
|
||
archiveURL, err := s.uploadToStoragesvc(ctx, data)
|
||
if err != nil {
|
||
log.Printf("storagesvc upload failed, falling back to literal: %v", err)
|
||
// fallback — сохраняем как literal
|
||
return map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(data)}, nil
|
||
}
|
||
if archiveURL == "" {
|
||
return map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(data)}, nil
|
||
}
|
||
return map[string]any{"type": "url", "url": archiveURL}, nil
|
||
}
|