package fission import ( "context" "encoding/base64" "fmt" "strconv" "time" "fission-console/internal/model" "fission-console/internal/runtime" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/dynamic" ) const ( functionCreatedAtAnnotation = "fission-console/createdAt" functionUpdatedAtAnnotation = "fission-console/updatedAt" ) // CreateFunctionResult holds the result of a CreateFunction call. type CreateFunctionResult struct { Name string `json:"name"` Package string `json:"package"` HTTPTrigger string `json:"httptrigger"` Route string `json:"route"` ExpiresAt string `json:"expires_at,omitempty"` } // UpdateFunctionResult holds the result of an UpdateFunctionCode call. type UpdateFunctionResult struct { Updated bool `json:"updated"` Package string `json:"package"` } // CreateFunction orchestrates the creation of a complete Fission function set: Package, Function, and HTTPTrigger. func CreateFunction(ctx context.Context, dyn dynamic.Interface, ns string, req model.CreateFunctionRequest) (*CreateFunctionResult, error) { pkgName := req.Name + "-pkg" triggerName := req.Name + "-route" methodValues := make([]any, 0, len(req.Methods)) for _, method := range req.Methods { methodValues = append(methodValues, method) } var pkgSpec map[string]any if req.Language == "go" { srcZip, err := runtime.BuildGoSourceZip(req.Code) if err != nil { return nil, fmt.Errorf("build go source archive: %w", err) } literal := base64.StdEncoding.EncodeToString(srcZip) pkgSpec = map[string]any{ "source": map[string]any{ "type": "literal", "literal": literal, }, "deployment": map[string]any{}, "environment": map[string]any{"name": req.Environment, "namespace": ns}, "buildcommand": "build", } } else { deployBytes, archiveErr := runtime.BuildDeployArchive(req.Language, req.Code) if archiveErr != nil { return nil, fmt.Errorf("build %s archive: %w", req.Language, archiveErr) } pkgSpec = map[string]any{ "deployment": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)}, "environment": map[string]any{"name": req.Environment, "namespace": ns}, "source": map[string]any{}, } } pkg := &unstructured.Unstructured{Object: map[string]any{ "apiVersion": "fission.io/v1", "kind": "Package", "metadata": map[string]any{"name": pkgName, "namespace": ns}, "spec": pkgSpec, }} fnAnnotations := map[string]any{ "fission-console/language": req.Language, } now := time.Now().UTC() fnAnnotations[functionCreatedAtAnnotation] = now.Format(time.RFC3339) fnAnnotations[functionUpdatedAtAnnotation] = now.Format(time.RFC3339) var expiresAtStr string if req.TTL != "" { expiresAt, ttlErr := parseTTL(req.TTL) if ttlErr != nil { return nil, fmt.Errorf("invalid ttl %q: %w", req.TTL, ttlErr) } expiresAtStr = expiresAt.UTC().Format(time.RFC3339) fnAnnotations["fission-console/expires-at"] = expiresAtStr } if _, err := dyn.Resource(PackageGVR).Namespace(ns).Create(ctx, pkg, metav1.CreateOptions{}); err != nil { return nil, fmt.Errorf("create package: %w", err) } fn := &unstructured.Unstructured{Object: map[string]any{ "apiVersion": "fission.io/v1", "kind": "Function", "metadata": map[string]any{"name": req.Name, "namespace": ns, "annotations": fnAnnotations}, "spec": map[string]any{ "environment": map[string]any{"name": req.Environment, "namespace": ns}, "functionTimeout": req.Timeout, "InvokeStrategy": map[string]any{ "ExecutionStrategy": map[string]any{"ExecutorType": "poolmgr"}, "StrategyType": "execution", }, "package": map[string]any{ "packageref": map[string]any{"name": pkgName, "namespace": ns}, "functionName": req.Entrypoint, }, }, }} if _, err := dyn.Resource(FunctionGVR).Namespace(ns).Create(ctx, fn, metav1.CreateOptions{}); err != nil { _ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), pkgName, metav1.DeleteOptions{}) return nil, fmt.Errorf("create function: %w", err) } httpTrigger := &unstructured.Unstructured{Object: map[string]any{ "apiVersion": "fission.io/v1", "kind": "HTTPTrigger", "metadata": map[string]any{"name": triggerName, "namespace": ns}, "spec": map[string]any{ "relativeurl": req.Route, "methods": methodValues, "createingress": true, "functionref": map[string]any{"type": "name", "name": req.Name}, }, }} if _, err := dyn.Resource(HTTPTrigGVR).Namespace(ns).Create(ctx, httpTrigger, metav1.CreateOptions{}); err != nil { _ = dyn.Resource(FunctionGVR).Namespace(ns).Delete(context.Background(), req.Name, metav1.DeleteOptions{}) _ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), pkgName, metav1.DeleteOptions{}) return nil, fmt.Errorf("create httptrigger: %w", err) } return &CreateFunctionResult{ Name: req.Name, Package: pkgName, HTTPTrigger: triggerName, Route: req.Route, ExpiresAt: expiresAtStr, }, nil } // UpdateFunctionCode orchestrates updating the code of an existing function. func UpdateFunctionCode(ctx context.Context, dyn dynamic.Interface, ns, name string, req model.UpdateCodeRequest) (*UpdateFunctionResult, error) { fn, err := dyn.Resource(FunctionGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("get function %q: %w", name, err) } oldPkgName, _, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name") lang, _, _ := unstructured.NestedString(fn.Object, "metadata", "annotations", "fission-console/language") deployBytes, archiveErr := runtime.BuildDeployArchive(lang, req.Code) if archiveErr != nil { return nil, fmt.Errorf("build %s archive: %w", lang, archiveErr) } envName, _, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name") newPkgName := name + "-pkg-" + strconv.FormatInt(time.Now().UnixMilli(), 36) newPkg := &unstructured.Unstructured{Object: map[string]any{ "apiVersion": "fission.io/v1", "kind": "Package", "metadata": map[string]any{"name": newPkgName, "namespace": ns}, "spec": map[string]any{ "deployment": map[string]any{"type": "literal", "literal": base64.StdEncoding.EncodeToString(deployBytes)}, "environment": map[string]any{"name": envName, "namespace": ns}, "source": map[string]any{}, }, }} createdPkg, err := dyn.Resource(PackageGVR).Namespace(ns).Create(ctx, newPkg, metav1.CreateOptions{}) if err != nil { return nil, fmt.Errorf("create new package: %w", err) } if err := unstructured.SetNestedField(fn.Object, req.Timeout, "spec", "functionTimeout"); err != nil { _ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), newPkgName, metav1.DeleteOptions{}) return nil, fmt.Errorf("set function timeout: %w", err) } annotations := fn.GetAnnotations() if annotations == nil { annotations = make(map[string]string) } annotations[functionUpdatedAtAnnotation] = time.Now().UTC().Format(time.RFC3339) fn.SetAnnotations(annotations) if err := unstructured.SetNestedField(fn.Object, map[string]any{ "name": newPkgName, "namespace": ns, "resourceversion": createdPkg.GetResourceVersion(), }, "spec", "package", "packageref"); err != nil { _ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), newPkgName, metav1.DeleteOptions{}) return nil, fmt.Errorf("set function packageref: %w", err) } if _, err := dyn.Resource(FunctionGVR).Namespace(ns).Update(ctx, fn, metav1.UpdateOptions{}); err != nil { _ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), newPkgName, metav1.DeleteOptions{}) return nil, fmt.Errorf("update function %q: %w", name, err) } if oldPkgName != "" && oldPkgName != newPkgName { _ = dyn.Resource(PackageGVR).Namespace(ns).Delete(context.Background(), oldPkgName, metav1.DeleteOptions{}) } return &UpdateFunctionResult{ Updated: true, Package: newPkgName, }, nil } // DeleteFunction orchestrates the deletion of a function and its related resources. func DeleteFunction(ctx context.Context, dyn dynamic.Interface, ns, name string) error { // Best-effort deletion of related resources. _ = dyn.Resource(HTTPTrigGVR).Namespace(ns).Delete(ctx, name+"-route", metav1.DeleteOptions{}) fn, err := dyn.Resource(FunctionGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) if err != nil && !apierrors.IsNotFound(err) { return fmt.Errorf("get function for deletion: %w", err) } if err == nil { if pkgName, found, _ := unstructured.NestedString(fn.Object, "spec", "package", "packageref", "name"); found && pkgName != "" { _ = dyn.Resource(PackageGVR).Namespace(ns).Delete(ctx, pkgName, metav1.DeleteOptions{}) } } if err := dyn.Resource(FunctionGVR).Namespace(ns).Delete(ctx, name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) { return fmt.Errorf("delete function: %w", err) } if fn != nil { if envName, found, _ := unstructured.NestedString(fn.Object, "spec", "environment", "name"); found && envName != "" { CleanupEnvironmentIfUnused(context.Background(), dyn, ns, envName) } } return nil } func parseTTL(ttl string) (time.Time, error) { duration, err := time.ParseDuration(ttl) if err != nil { return time.Time{}, err } return time.Now().UTC().Add(duration), nil }