package apps import ( "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) // App represents an application in the catalog type App struct { Name string `yaml:"name"` Description string `yaml:"description"` Version string `yaml:"version"` Category string `yaml:"category"` Homepage string `yaml:"homepage"` Source string `yaml:"source"` Tags []string `yaml:"tags"` Requires []string `yaml:"requires"` Provides map[string]string `yaml:"provides"` Config map[string]interface{} `yaml:"config"` } // Catalog manages the application catalog type Catalog struct { cacheDir string apps []App loaded bool } // NewCatalog creates a new app catalog func NewCatalog(cacheDir string) *Catalog { return &Catalog{ cacheDir: cacheDir, } } // LoadCatalog loads the app catalog from cache or remote source func (c *Catalog) LoadCatalog() error { if c.loaded { return nil } // Try to load from cache first catalogPath := filepath.Join(c.cacheDir, "catalog.yaml") if err := c.loadFromFile(catalogPath); err == nil { c.loaded = true return nil } // If cache fails, try to fetch from remote if err := c.fetchRemoteCatalog(); err != nil { return fmt.Errorf("failed to load catalog: %w", err) } c.loaded = true return nil } // loadFromFile loads catalog from a local file func (c *Catalog) loadFromFile(path string) error { data, err := os.ReadFile(path) if err != nil { return fmt.Errorf("reading catalog file: %w", err) } var catalogData struct { Apps []App `yaml:"apps"` } if err := yaml.Unmarshal(data, &catalogData); err != nil { return fmt.Errorf("parsing catalog YAML: %w", err) } c.apps = catalogData.Apps return nil } // fetchRemoteCatalog fetches catalog from remote source func (c *Catalog) fetchRemoteCatalog() error { // For now, create a default catalog // In production, this would fetch from a remote URL defaultCatalog := []App{ { Name: "nextcloud", Description: "Self-hosted file sync and share platform", Version: "latest", Category: "productivity", Homepage: "https://nextcloud.com", Source: "https://github.com/wild-cloud/app-nextcloud", Tags: []string{"files", "sync", "collaboration"}, Requires: []string{"postgresql"}, Provides: map[string]string{"files": "nextcloud"}, }, { Name: "postgresql", Description: "Powerful, open source object-relational database", Version: "15", Category: "database", Homepage: "https://postgresql.org", Source: "https://github.com/wild-cloud/app-postgresql", Tags: []string{"database", "sql"}, Provides: map[string]string{"database": "postgresql"}, }, { Name: "traefik", Description: "Modern HTTP reverse proxy and load balancer", Version: "v3.0", Category: "infrastructure", Homepage: "https://traefik.io", Source: "https://github.com/wild-cloud/app-traefik", Tags: []string{"proxy", "loadbalancer", "ingress"}, Provides: map[string]string{"ingress": "traefik"}, }, { Name: "monitoring", Description: "Prometheus and Grafana monitoring stack", Version: "latest", Category: "infrastructure", Homepage: "https://prometheus.io", Source: "https://github.com/wild-cloud/app-monitoring", Tags: []string{"monitoring", "metrics", "alerting"}, Provides: map[string]string{"monitoring": "prometheus"}, }, } c.apps = defaultCatalog // Save to cache return c.saveCatalogToCache() } // saveCatalogToCache saves the catalog to cache func (c *Catalog) saveCatalogToCache() error { catalogData := struct { Apps []App `yaml:"apps"` }{ Apps: c.apps, } data, err := yaml.Marshal(catalogData) if err != nil { return fmt.Errorf("marshaling catalog: %w", err) } catalogPath := filepath.Join(c.cacheDir, "catalog.yaml") if err := os.MkdirAll(filepath.Dir(catalogPath), 0755); err != nil { return fmt.Errorf("creating cache directory: %w", err) } if err := os.WriteFile(catalogPath, data, 0644); err != nil { return fmt.Errorf("writing catalog file: %w", err) } return nil } // ListApps returns all apps in the catalog func (c *Catalog) ListApps() ([]App, error) { if err := c.LoadCatalog(); err != nil { return nil, err } return c.apps, nil } // FindApp finds an app by name func (c *Catalog) FindApp(name string) (*App, error) { if err := c.LoadCatalog(); err != nil { return nil, err } for _, app := range c.apps { if app.Name == name { return &app, nil } } return nil, fmt.Errorf("app '%s' not found in catalog", name) } // SearchApps searches for apps by name or tag func (c *Catalog) SearchApps(query string) ([]App, error) { if err := c.LoadCatalog(); err != nil { return nil, err } var results []App query = strings.ToLower(query) for _, app := range c.apps { // Check name if strings.Contains(strings.ToLower(app.Name), query) { results = append(results, app) continue } // Check description if strings.Contains(strings.ToLower(app.Description), query) { results = append(results, app) continue } // Check tags for _, tag := range app.Tags { if strings.Contains(strings.ToLower(tag), query) { results = append(results, app) break } } } return results, nil } // FetchApp downloads an app template to cache func (c *Catalog) FetchApp(name string) error { app, err := c.FindApp(name) if err != nil { return err } appCacheDir := filepath.Join(c.cacheDir, "apps", name) if err := os.MkdirAll(appCacheDir, 0755); err != nil { return fmt.Errorf("creating app cache directory: %w", err) } // For now, create a basic app template // In production, this would clone from app.Source if err := c.createAppTemplate(app, appCacheDir); err != nil { return fmt.Errorf("creating app template: %w", err) } return nil } // createAppTemplate creates a basic app template structure func (c *Catalog) createAppTemplate(app *App, dir string) error { // Create manifest.yaml manifest := map[string]interface{}{ "name": app.Name, "version": app.Version, "description": app.Description, "requires": app.Requires, "provides": app.Provides, "config": app.Config, } manifestData, err := yaml.Marshal(manifest) if err != nil { return fmt.Errorf("marshaling manifest: %w", err) } manifestPath := filepath.Join(dir, "manifest.yaml") if err := os.WriteFile(manifestPath, manifestData, 0644); err != nil { return fmt.Errorf("writing manifest: %w", err) } // Create basic kubernetes manifests if err := c.createKubernetesManifests(app, dir); err != nil { return fmt.Errorf("creating kubernetes manifests: %w", err) } return nil } // createKubernetesManifests creates basic Kubernetes manifest templates func (c *Catalog) createKubernetesManifests(app *App, dir string) error { // Create namespace.yaml namespace := fmt.Sprintf(`apiVersion: v1 kind: Namespace metadata: name: %s labels: app: %s `, app.Name, app.Name) if err := os.WriteFile(filepath.Join(dir, "namespace.yaml"), []byte(namespace), 0644); err != nil { return fmt.Errorf("writing namespace.yaml: %w", err) } // Create basic deployment template deployment := fmt.Sprintf(`apiVersion: apps/v1 kind: Deployment metadata: name: %s namespace: %s spec: replicas: {{.config.%s.replicas | default 1}} selector: matchLabels: app: %s template: metadata: labels: app: %s spec: containers: - name: %s image: {{.config.%s.image | default "%s:latest"}} ports: - containerPort: 8080 `, app.Name, app.Name, app.Name, app.Name, app.Name, app.Name, app.Name, app.Name) if err := os.WriteFile(filepath.Join(dir, "deployment.yaml"), []byte(deployment), 0644); err != nil { return fmt.Errorf("writing deployment.yaml: %w", err) } // Create service.yaml service := fmt.Sprintf(`apiVersion: v1 kind: Service metadata: name: %s namespace: %s spec: selector: app: %s ports: - port: 80 targetPort: 8080 type: ClusterIP `, app.Name, app.Name, app.Name) if err := os.WriteFile(filepath.Join(dir, "service.yaml"), []byte(service), 0644); err != nil { return fmt.Errorf("writing service.yaml: %w", err) } // Create kustomization.yaml kustomization := `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - namespace.yaml - deployment.yaml - service.yaml ` if err := os.WriteFile(filepath.Join(dir, "kustomization.yaml"), []byte(kustomization), 0644); err != nil { return fmt.Errorf("writing kustomization.yaml: %w", err) } return nil } // IsAppCached checks if an app is cached locally func (c *Catalog) IsAppCached(name string) bool { appCacheDir := filepath.Join(c.cacheDir, "apps", name) manifestPath := filepath.Join(appCacheDir, "manifest.yaml") _, err := os.Stat(manifestPath) return err == nil } // GetCachedApps returns list of cached apps func (c *Catalog) GetCachedApps() ([]string, error) { appsDir := filepath.Join(c.cacheDir, "apps") entries, err := os.ReadDir(appsDir) if err != nil { if os.IsNotExist(err) { return []string{}, nil } return nil, fmt.Errorf("reading apps directory: %w", err) } var cachedApps []string for _, entry := range entries { if entry.IsDir() { manifestPath := filepath.Join(appsDir, entry.Name(), "manifest.yaml") if _, err := os.Stat(manifestPath); err == nil { cachedApps = append(cachedApps, entry.Name()) } } } return cachedApps, nil }