// 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 }