Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions controllers/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@ type deployAppRequest struct {
}

type deployAppResult struct {
Deployment deploymentSummary `json:"deployment"`
Service *serviceSummary `json:"service,omitempty"`
Deployment *deploymentSummary `json:"deployment,omitempty"`
Service *serviceSummary `json:"service,omitempty"`
}

// DeployApp creates a Deployment and a matching ClusterIP/NodePort Service in one call.
// Helm charts are installed through /api/install-helm-chart instead.
// @router /api/deploy-app [post]
func (c *ApiController) DeployApp() {
if c.RequireAdmin() {
return
}
cfg := getAdminRestConfig()
if cfg == nil {
c.ResponseError("apiserver not ready")
Expand Down Expand Up @@ -82,7 +86,8 @@ func (c *ApiController) DeployApp() {
return
}

result := deployAppResult{Deployment: toDeploymentSummary(*createdDepl)}
deplSummary := toDeploymentSummary(*createdDepl)
result := deployAppResult{Deployment: &deplSummary}

if len(req.Ports) > 0 {
svcPorts := make([]portRequest, 0, len(req.Ports))
Expand Down
94 changes: 94 additions & 0 deletions controllers/app_artifacthub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package controllers

import (
"fmt"
"sort"
"strings"

"github.com/casosorg/casos/object"
)

// fetchArtifactHubTemplates returns a curated first page of popular Helm charts
// for the app store landing view. Full discovery goes through
// /api/search-helm-charts, which queries ArtifactHub server-side.
func fetchArtifactHubTemplates() ([]AppTemplate, error) {
result, err := object.SearchArtifactHubCharts("", 1, 48)
if err != nil {
return nil, err
}

templates := make([]AppTemplate, 0, len(result.Items))
seen := map[string]bool{}
for _, item := range result.Items {
key := item.RepoURL + "/" + item.ChartName
if seen[key] {
continue
}
seen[key] = true
templates = append(templates, toAppTemplate(item))
}

sort.SliceStable(templates, func(i, j int) bool {
return strings.ToLower(templates[i].Title) < strings.ToLower(templates[j].Title)
})
return templates, nil
}

func toAppTemplate(item object.HelmChartListItem) AppTemplate {
categories := append([]string{"Helm"}, item.Categories...)
if item.Official {
categories = append(categories, "Official")
} else if item.Verified {
categories = append(categories, "Verified")
}

return AppTemplate{
Name: safeAppTemplateName(item.ChartName),
Title: item.Title,
Description: item.Description,
Icon: item.Icon,
Categories: categories,
Source: item.Source,
PackageType: item.PackageType,
RepoName: item.RepoName,
RepoURL: item.RepoURL,
ChartName: item.ChartName,
Version: item.Version,
}
}

func safeAppTemplateName(name string) string {
name = strings.ToLower(strings.TrimSpace(name))
var b strings.Builder
lastDash := false
for _, r := range name {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
lastDash = false
continue
}
if !lastDash {
b.WriteByte('-')
lastDash = true
}
}
result := strings.Trim(b.String(), "-")
if result == "" {
return "app"
}
return result
}

func helmChartRefFromRequest(repoURL, chartName, version, username, password string) (object.HelmChartRef, error) {
ref := object.HelmChartRef{
RepoURL: strings.TrimSpace(repoURL),
Chart: strings.TrimSpace(chartName),
Version: strings.TrimSpace(version),
Username: username,
Password: password,
}
if _, _, err := ref.Normalize(); err != nil {
return object.HelmChartRef{}, fmt.Errorf("invalid chart reference: %w", err)
}
return ref, nil
}
117 changes: 108 additions & 9 deletions controllers/apptemplate.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package controllers

import (
"context"
"encoding/json"
"fmt"
"io"
Expand All @@ -15,9 +16,17 @@ import (
)

const (
githubTreeURL = "https://api.github.com/repos/labring-actions/templates/git/trees/main?recursive=1"
rawContentBase = "https://raw.githubusercontent.com/labring-actions/templates/main/"
templateCacheTTL = time.Hour
githubTreeURL = "https://api.github.com/repos/labring-actions/templates/git/trees/main?recursive=1"
rawContentBase = "https://raw.githubusercontent.com/labring-actions/templates/main/"
artifactHubSearchURL = "https://artifacthub.io/api/v1/packages/search?kind=0&limit=48&offset=0&sort=relevance"
templateCacheTTL = time.Hour
)

const (
appSourceSealos = "sealos"
appSourceArtifactHub = "artifacthub"
appPackageSealos = "sealos-template"
appPackageHelm = "helm"
)

type TemplateInput struct {
Expand All @@ -37,6 +46,12 @@ type AppTemplate struct {
Image string `json:"image"`
Ports []int32 `json:"ports"`
Inputs []TemplateInput `json:"inputs"`
Source string `json:"source"`
PackageType string `json:"packageType"`
RepoName string `json:"repoName,omitempty"`
RepoURL string `json:"repoUrl,omitempty"`
ChartName string `json:"chartName,omitempty"`
Version string `json:"version,omitempty"`
}

var (
Expand All @@ -45,7 +60,7 @@ var (
templateCacheMu sync.RWMutex
)

// GetAppTemplates returns the Sealos app template catalog from GitHub.
// GetAppTemplates returns app templates from the configured app store sources.
// Results are cached in memory for 1 hour.
// @router /api/get-app-templates [get]
func (c *ApiController) GetAppTemplates() {
Expand All @@ -66,19 +81,101 @@ func getCachedTemplates() ([]AppTemplate, error) {
}
templateCacheMu.RUnlock()

templates, err := fetchAllTemplates()
templates, partial, err := fetchAllTemplateSources()
if err != nil {
return nil, err
}

templateCacheMu.Lock()
templateCache = templates
templateCacheAt = time.Now()
templateCacheMu.Unlock()
if !partial {
templateCacheMu.Lock()
templateCache = templates
templateCacheAt = time.Now()
templateCacheMu.Unlock()
}

return templates, nil
}

type templateSourceResult struct {
source string
templates []AppTemplate
err error
}

func fetchAllTemplateSources() ([]AppTemplate, bool, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

resultCh := make(chan templateSourceResult, 2)
sources := []struct {
name string
fetch func() ([]AppTemplate, error)
}{
{name: appSourceSealos, fetch: fetchAllTemplates},
{name: appSourceArtifactHub, fetch: fetchArtifactHubTemplates},
}

for _, source := range sources {
source := source
go func() {
templates, err := source.fetch()
select {
case resultCh <- templateSourceResult{source: source.name, templates: templates, err: err}:
case <-ctx.Done():
}
}()
}

hardTimer := time.NewTimer(20 * time.Second)
defer hardTimer.Stop()
softTimer := time.NewTimer(5 * time.Second)
defer softTimer.Stop()
softTimeout := softTimer.C
hardTimeout := hardTimer.C

sourceTemplates := map[string][]AppTemplate{}
var errs []string
completed := 0
templateCount := 0
partial := false
for completed < len(sources) {
select {
case result := <-resultCh:
completed++
if result.err != nil {
errs = append(errs, fmt.Sprintf("%s: %s", result.source, result.err.Error()))
continue
}
sourceTemplates[result.source] = result.templates
templateCount += len(result.templates)
case <-softTimeout:
if templateCount > 0 {
partial = completed < len(sources)
completed = len(sources)
} else {
softTimeout = nil
}
case <-hardTimeout:
errs = append(errs, "app template source timeout")
partial = completed < len(sources)
completed = len(sources)
}
}

var templates []AppTemplate
for _, source := range sources {
templates = append(templates, sourceTemplates[source.name]...)
}

if len(templates) == 0 {
if len(errs) == 0 {
return nil, false, fmt.Errorf("no app templates found")
}
return nil, false, fmt.Errorf("fetch app templates: %s", strings.Join(errs, "; "))
}
return templates, partial || len(errs) > 0, nil
}

type githubTree struct {
Tree []struct {
Path string `json:"path"`
Expand Down Expand Up @@ -279,5 +376,7 @@ func fetchAndParseTemplate(path string) (AppTemplate, error) {
Image: image,
Ports: ports,
Inputs: inputs,
Source: appSourceSealos,
PackageType: appPackageSealos,
}, nil
}
Loading