First commit of golang CLI.
This commit is contained in:
378
wild-cli/internal/apps/catalog.go
Normal file
378
wild-cli/internal/apps/catalog.go
Normal file
@@ -0,0 +1,378 @@
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user