Compare commits

..

2 Commits

Author SHA1 Message Date
Paul Payne
ff97f14229 Get templates from embedded package. 2025-10-11 22:20:26 +00:00
Paul Payne
89c6a7aa80 Moves setup files into embedded package. 2025-10-11 22:06:39 +00:00
118 changed files with 497 additions and 272 deletions

157
README.md
View File

@@ -10,159 +10,4 @@ make dev
## Usage
### Batch Configuration Update Endpoint
#### Overview
The batch configuration update endpoint allows updating multiple configuration values in a single atomic request.
#### Endpoint
```
PATCH /api/v1/instances/{name}/config
```
#### Request Format
```json
{
"updates": [
{"path": "string", "value": "any"},
{"path": "string", "value": "any"}
]
}
```
#### Response Format
Success (200 OK):
```json
{
"message": "Configuration updated successfully",
"updated": 3
}
```
Error (400 Bad Request / 404 Not Found / 500 Internal Server Error):
```json
{
"error": "error message"
}
```
#### Usage Examples
##### Example 1: Update Basic Configuration Values
```bash
curl -X PATCH http://localhost:8080/api/v1/instances/my-cloud/config \
-H "Content-Type: application/json" \
-d '{
"updates": [
{"path": "baseDomain", "value": "example.com"},
{"path": "domain", "value": "wild.example.com"},
{"path": "internalDomain", "value": "int.wild.example.com"}
]
}'
```
Response:
```json
{
"message": "Configuration updated successfully",
"updated": 3
}
```
##### Example 2: Update Nested Configuration Values
```bash
curl -X PATCH http://localhost:8080/api/v1/instances/my-cloud/config \
-H "Content-Type: application/json" \
-d '{
"updates": [
{"path": "cluster.name", "value": "prod-cluster"},
{"path": "cluster.loadBalancerIp", "value": "192.168.1.100"},
{"path": "cluster.ipAddressPool", "value": "192.168.1.100-192.168.1.200"}
]
}'
```
##### Example 3: Update Array Values
```bash
curl -X PATCH http://localhost:8080/api/v1/instances/my-cloud/config \
-H "Content-Type: application/json" \
-d '{
"updates": [
{"path": "cluster.nodes.activeNodes[0]", "value": "node-1"},
{"path": "cluster.nodes.activeNodes[1]", "value": "node-2"}
]
}'
```
##### Example 4: Error Handling - Invalid Instance
```bash
curl -X PATCH http://localhost:8080/api/v1/instances/nonexistent/config \
-H "Content-Type: application/json" \
-d '{
"updates": [
{"path": "baseDomain", "value": "example.com"}
]
}'
```
Response (404):
```json
{
"error": "Instance not found: instance nonexistent does not exist"
}
```
##### Example 5: Error Handling - Empty Updates
```bash
curl -X PATCH http://localhost:8080/api/v1/instances/my-cloud/config \
-H "Content-Type: application/json" \
-d '{
"updates": []
}'
```
Response (400):
```json
{
"error": "updates array is required and cannot be empty"
}
```
##### Example 6: Error Handling - Missing Path
```bash
curl -X PATCH http://localhost:8080/api/v1/instances/my-cloud/config \
-H "Content-Type: application/json" \
-d '{
"updates": [
{"path": "", "value": "example.com"}
]
}'
```
Response (400):
```json
{
"error": "update[0]: path is required"
}
```
#### Configuration Path Syntax
The `path` field uses YAML path syntax as implemented by the `yq` tool:
- Simple fields: `baseDomain`
- Nested fields: `cluster.name`
- Array elements: `cluster.nodes.activeNodes[0]`
- Array append: `cluster.nodes.activeNodes[+]`
Refer to the yq documentation for advanced path syntax.
TBD

View File

@@ -21,36 +21,32 @@ import (
// API holds all dependencies for API handlers
type API struct {
dataDir string
directoryPath string // Path to Wild Cloud Directory
appsDir string
config *config.Manager
secrets *secrets.Manager
context *context.Manager
instance *instance.Manager
broadcaster *operations.Broadcaster // SSE broadcaster for operation output
dataDir string
appsDir string // Path to external apps directory
config *config.Manager
secrets *secrets.Manager
context *context.Manager
instance *instance.Manager
broadcaster *operations.Broadcaster // SSE broadcaster for operation output
}
// NewAPI creates a new API handler with all dependencies
func NewAPI(dataDir, directoryPath string) (*API, error) {
// Note: Setup files (cluster-services, cluster-nodes, etc.) are now embedded in the binary
func NewAPI(dataDir, appsDir string) (*API, error) {
// Ensure base directories exist
instancesDir := filepath.Join(dataDir, "instances")
if err := os.MkdirAll(instancesDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create instances directory: %w", err)
}
// Apps directory is now in Wild Cloud Directory
appsDir := filepath.Join(directoryPath, "apps")
return &API{
dataDir: dataDir,
directoryPath: directoryPath,
appsDir: appsDir,
config: config.NewManager(),
secrets: secrets.NewManager(),
context: context.NewManager(dataDir),
instance: instance.NewManager(dataDir),
broadcaster: operations.NewBroadcaster(),
dataDir: dataDir,
appsDir: appsDir,
config: config.NewManager(),
secrets: secrets.NewManager(),
context: context.NewManager(dataDir),
instance: instance.NewManager(dataDir),
broadcaster: operations.NewBroadcaster(),
}, nil
}
@@ -427,7 +423,7 @@ func (api *API) SetContext(w http.ResponseWriter, r *http.Request) {
}
// StatusHandler returns daemon status information
func (api *API) StatusHandler(w http.ResponseWriter, r *http.Request, startTime time.Time, dataDir, directoryPath string) {
func (api *API) StatusHandler(w http.ResponseWriter, r *http.Request, startTime time.Time, dataDir, appsDir string) {
// Get list of instances
instances, err := api.instance.ListInstances()
if err != nil {
@@ -443,7 +439,8 @@ func (api *API) StatusHandler(w http.ResponseWriter, r *http.Request, startTime
"uptime": uptime.String(),
"uptimeSeconds": int(uptime.Seconds()),
"dataDir": dataDir,
"directoryPath": directoryPath,
"appsDir": appsDir,
"setupFiles": "embedded", // Indicate that setup files are now embedded
"instances": map[string]interface{}{
"count": len(instances),
"names": instances,

View File

@@ -27,7 +27,7 @@ func (api *API) ServicesList(w http.ResponseWriter, r *http.Request) {
}
// List services
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
servicesMgr := services.NewManager(api.dataDir)
svcList, err := servicesMgr.List(instanceName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list services: %v", err))
@@ -52,7 +52,7 @@ func (api *API) ServicesGet(w http.ResponseWriter, r *http.Request) {
}
// Get service
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
servicesMgr := services.NewManager(api.dataDir)
service, err := servicesMgr.Get(instanceName, serviceName)
if err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Service not found: %v", err))
@@ -109,7 +109,7 @@ func (api *API) ServicesInstall(w http.ResponseWriter, r *http.Request) {
}()
fmt.Printf("[DEBUG] Service install goroutine started: service=%s instance=%s opID=%s\n", req.Name, instanceName, opID)
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
servicesMgr := services.NewManager(api.dataDir)
opsMgr.UpdateStatus(instanceName, opID, "running")
if err := servicesMgr.Install(instanceName, req.Name, req.Fetch, req.Deploy, opID, api.broadcaster); err != nil {
@@ -159,7 +159,7 @@ func (api *API) ServicesInstallAll(w http.ResponseWriter, r *http.Request) {
// Install in background
go func() {
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
servicesMgr := services.NewManager(api.dataDir)
opsMgr.UpdateStatus(instanceName, opID, "running")
if err := servicesMgr.InstallAll(instanceName, req.Fetch, req.Deploy, opID, api.broadcaster); err != nil {
@@ -197,7 +197,7 @@ func (api *API) ServicesDelete(w http.ResponseWriter, r *http.Request) {
// Delete in background
go func() {
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
servicesMgr := services.NewManager(api.dataDir)
opsMgr.UpdateStatus(instanceName, opID, "running")
if err := servicesMgr.Delete(instanceName, serviceName); err != nil {
@@ -226,7 +226,7 @@ func (api *API) ServicesGetStatus(w http.ResponseWriter, r *http.Request) {
}
// Get status
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
servicesMgr := services.NewManager(api.dataDir)
status, err := servicesMgr.GetStatus(instanceName, serviceName)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get status: %v", err))
@@ -241,7 +241,7 @@ func (api *API) ServicesGetManifest(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
serviceName := vars["service"]
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
servicesMgr := services.NewManager(api.dataDir)
manifest, err := servicesMgr.GetManifest(serviceName)
if err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Service not found: %v", err))
@@ -256,7 +256,7 @@ func (api *API) ServicesGetConfig(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
serviceName := vars["service"]
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
servicesMgr := services.NewManager(api.dataDir)
// Get manifest
manifest, err := servicesMgr.GetManifest(serviceName)
@@ -286,7 +286,7 @@ func (api *API) ServicesGetInstanceConfig(w http.ResponseWriter, r *http.Request
return
}
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
servicesMgr := services.NewManager(api.dataDir)
// Get manifest to know which config paths to read
manifest, err := servicesMgr.GetManifest(serviceName)
@@ -364,7 +364,7 @@ func (api *API) ServicesFetch(w http.ResponseWriter, r *http.Request) {
}
// Fetch service files
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
servicesMgr := services.NewManager(api.dataDir)
if err := servicesMgr.Fetch(instanceName, serviceName); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to fetch service: %v", err))
return
@@ -388,7 +388,7 @@ func (api *API) ServicesCompile(w http.ResponseWriter, r *http.Request) {
}
// Compile templates
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
servicesMgr := services.NewManager(api.dataDir)
if err := servicesMgr.Compile(instanceName, serviceName); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to compile templates: %v", err))
return
@@ -412,7 +412,7 @@ func (api *API) ServicesDeploy(w http.ResponseWriter, r *http.Request) {
}
// Deploy service (without operation tracking for standalone deploy)
servicesMgr := services.NewManager(api.dataDir, filepath.Join(api.directoryPath, "setup", "cluster-services"))
servicesMgr := services.NewManager(api.dataDir)
if err := servicesMgr.Deploy(instanceName, serviceName, "", nil); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to deploy service: %v", err))
return

View File

@@ -8,6 +8,7 @@ import (
"strings"
"github.com/wild-cloud/wild-central/daemon/internal/config"
"github.com/wild-cloud/wild-central/daemon/internal/setup"
"github.com/wild-cloud/wild-central/daemon/internal/tools"
)
@@ -335,11 +336,11 @@ func (m *Manager) Apply(instanceName, nodeIdentifier string, opts ApplyOptions)
}
}
// Always auto-fetch templates if they don't exist
// Always auto-extract templates from embedded files if they don't exist
templatesDir := filepath.Join(setupDir, "patch.templates")
if !m.templatesExist(templatesDir) {
if err := m.copyTemplatesFromDirectory(templatesDir); err != nil {
return fmt.Errorf("failed to copy templates: %w", err)
if err := m.extractEmbeddedTemplates(templatesDir); err != nil {
return fmt.Errorf("failed to extract templates: %w", err)
}
}
@@ -504,36 +505,31 @@ func (m *Manager) templatesExist(templatesDir string) bool {
return err1 == nil && err2 == nil
}
// copyTemplatesFromDirectory copies patch templates from directory/ to instance
func (m *Manager) copyTemplatesFromDirectory(destDir string) error {
// Find the directory/setup/cluster-nodes/patch.templates directory
// It should be in the same parent as the data directory
sourceDir := filepath.Join(filepath.Dir(m.dataDir), "directory", "setup", "cluster-nodes", "patch.templates")
// Check if source directory exists
if _, err := os.Stat(sourceDir); err != nil {
return fmt.Errorf("source templates directory not found: %s", sourceDir)
}
// extractEmbeddedTemplates extracts patch templates from embedded files to instance directory
func (m *Manager) extractEmbeddedTemplates(destDir string) error {
// Create destination directory
if err := os.MkdirAll(destDir, 0755); err != nil {
return fmt.Errorf("failed to create templates directory: %w", err)
}
// Copy controlplane.yaml
if err := m.copyFile(
filepath.Join(sourceDir, "controlplane.yaml"),
filepath.Join(destDir, "controlplane.yaml"),
); err != nil {
return fmt.Errorf("failed to copy controlplane template: %w", err)
// Get embedded template files
controlplaneData, err := setup.GetClusterNodesFile("patch.templates/controlplane.yaml")
if err != nil {
return fmt.Errorf("failed to get controlplane template: %w", err)
}
// Copy worker.yaml
if err := m.copyFile(
filepath.Join(sourceDir, "worker.yaml"),
filepath.Join(destDir, "worker.yaml"),
); err != nil {
return fmt.Errorf("failed to copy worker template: %w", err)
workerData, err := setup.GetClusterNodesFile("patch.templates/worker.yaml")
if err != nil {
return fmt.Errorf("failed to get worker template: %w", err)
}
// Write templates
if err := os.WriteFile(filepath.Join(destDir, "controlplane.yaml"), controlplaneData, 0644); err != nil {
return fmt.Errorf("failed to write controlplane template: %w", err)
}
if err := os.WriteFile(filepath.Join(destDir, "worker.yaml"), workerData, 0644); err != nil {
return fmt.Errorf("failed to write worker template: %w", err)
}
return nil
@@ -660,9 +656,9 @@ func (m *Manager) Update(instanceName string, hostname string, updates map[strin
return nil
}
// FetchTemplates copies patch templates from directory/ to instance
// FetchTemplates extracts patch templates from embedded files to instance
func (m *Manager) FetchTemplates(instanceName string) error {
instancePath := m.GetInstancePath(instanceName)
destDir := filepath.Join(instancePath, "setup", "cluster-nodes", "patch.templates")
return m.copyTemplatesFromDirectory(destDir)
return m.extractEmbeddedTemplates(destDir)
}

View File

@@ -2,6 +2,7 @@ package services
import (
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
@@ -10,30 +11,41 @@ import (
"gopkg.in/yaml.v3"
"github.com/wild-cloud/wild-central/daemon/internal/operations"
"github.com/wild-cloud/wild-central/daemon/internal/setup"
"github.com/wild-cloud/wild-central/daemon/internal/storage"
"github.com/wild-cloud/wild-central/daemon/internal/tools"
)
// Manager handles base service operations
type Manager struct {
dataDir string
servicesDir string // Path to services directory
manifests map[string]*ServiceManifest // Cached service manifests
dataDir string
manifests map[string]*ServiceManifest // Cached service manifests
}
// NewManager creates a new services manager
func NewManager(dataDir, servicesDir string) *Manager {
// Note: Service definitions are now loaded from embedded setup files
func NewManager(dataDir string) *Manager {
m := &Manager{
dataDir: dataDir,
servicesDir: servicesDir,
dataDir: dataDir,
}
// Load all service manifests
manifests, err := LoadAllManifests(servicesDir)
if err != nil {
// Log error but continue - services without manifests will fall back to hardcoded map
fmt.Printf("Warning: failed to load service manifests: %v\n", err)
manifests = make(map[string]*ServiceManifest)
// Load all service manifests from embedded files
manifests := make(map[string]*ServiceManifest)
services, err := setup.ListServices()
if err == nil {
for _, serviceName := range services {
manifest, err := setup.GetManifest(serviceName)
if err == nil {
// Convert setup.ServiceManifest to services.ServiceManifest
manifests[serviceName] = &ServiceManifest{
Name: manifest.Name,
Description: manifest.Description,
Category: manifest.Category,
}
}
}
} else {
fmt.Printf("Warning: failed to load service manifests from embedded files: %v\n", err)
}
m.manifests = manifests
@@ -124,18 +136,13 @@ func (m *Manager) checkServiceStatus(instanceName, serviceName string) string {
func (m *Manager) List(instanceName string) ([]Service, error) {
services := []Service{}
// Discover services from the services directory
entries, err := os.ReadDir(m.servicesDir)
// Discover services from embedded setup files
serviceNames, err := setup.ListServices()
if err != nil {
return nil, fmt.Errorf("failed to read services directory: %w", err)
return nil, fmt.Errorf("failed to list services from embedded files: %w", err)
}
for _, entry := range entries {
if !entry.IsDir() {
continue // Skip non-directories like README.md
}
name := entry.Name()
for _, name := range serviceNames {
// Get service info from manifest if available
var namespace, description, version string
@@ -232,11 +239,17 @@ func (m *Manager) InstallAll(instanceName string, fetch, deploy bool, opID strin
func (m *Manager) Delete(instanceName, serviceName string) error {
kubeconfigPath := tools.GetKubeconfigPath(m.dataDir, instanceName)
serviceDir := filepath.Join(m.servicesDir, serviceName)
manifestsFile := filepath.Join(serviceDir, "manifests.yaml")
// Check if service exists in embedded files
if !setup.ServiceExists(serviceName) {
return fmt.Errorf("service %s not found", serviceName)
}
// Get manifests file from embedded setup or instance directory
instanceServiceDir := filepath.Join(m.dataDir, "instances", instanceName, "setup", "cluster-services", serviceName)
manifestsFile := filepath.Join(instanceServiceDir, "manifests.yaml")
if !storage.FileExists(manifestsFile) {
return fmt.Errorf("service %s not found", serviceName)
return fmt.Errorf("service manifests not found - service may not be installed")
}
cmd := exec.Command("kubectl", "delete", "-f", manifestsFile)
@@ -292,12 +305,11 @@ func (m *Manager) GetConfigReferences(serviceName string) ([]string, error) {
return manifest.ConfigReferences, nil
}
// Fetch copies service files from directory to instance
// Fetch extracts service files from embedded setup to instance
func (m *Manager) Fetch(instanceName, serviceName string) error {
// 1. Validate service exists in directory
sourceDir := filepath.Join(m.servicesDir, serviceName)
if !dirExists(sourceDir) {
return fmt.Errorf("service %s not found in directory", serviceName)
// 1. Validate service exists in embedded files
if !setup.ServiceExists(serviceName) {
return fmt.Errorf("service %s not found in embedded files", serviceName)
}
// 2. Create instance service directory
@@ -307,31 +319,36 @@ func (m *Manager) Fetch(instanceName, serviceName string) error {
return fmt.Errorf("failed to create service directory: %w", err)
}
// 3. Copy files:
// 3. Extract files from embedded setup:
// - README.md (if exists, optional)
// - install.sh (if exists, optional)
// - wild-manifest.yaml
// - kustomize.template/* (if exists, optional)
// Copy README.md
copyFileIfExists(filepath.Join(sourceDir, "README.md"),
filepath.Join(instanceDir, "README.md"))
// Copy install.sh (optional)
installSh := filepath.Join(sourceDir, "install.sh")
if fileExists(installSh) {
if err := copyFile(installSh, filepath.Join(instanceDir, "install.sh")); err != nil {
return fmt.Errorf("failed to copy install.sh: %w", err)
}
// Make install.sh executable
os.Chmod(filepath.Join(instanceDir, "install.sh"), 0755)
// Extract README.md if it exists
if readmeData, err := setup.GetServiceFile(serviceName, "README.md"); err == nil {
os.WriteFile(filepath.Join(instanceDir, "README.md"), readmeData, 0644)
}
// Copy kustomize.template directory if it exists
templateDir := filepath.Join(sourceDir, "kustomize.template")
if dirExists(templateDir) {
// Extract install.sh if it exists
if installData, err := setup.GetServiceFile(serviceName, "install.sh"); err == nil {
installPath := filepath.Join(instanceDir, "install.sh")
if err := os.WriteFile(installPath, installData, 0755); err != nil {
return fmt.Errorf("failed to write install.sh: %w", err)
}
}
// Extract wild-manifest.yaml
if manifestData, err := setup.GetServiceFile(serviceName, "wild-manifest.yaml"); err == nil {
os.WriteFile(filepath.Join(instanceDir, "wild-manifest.yaml"), manifestData, 0644)
}
// Extract kustomize.template directory
templateFS, err := setup.GetKustomizeTemplate(serviceName)
if err == nil {
destTemplateDir := filepath.Join(instanceDir, "kustomize.template")
if err := copyDir(templateDir, destTemplateDir); err != nil {
return fmt.Errorf("failed to copy templates: %w", err)
if err := extractFS(templateFS, destTemplateDir); err != nil {
return fmt.Errorf("failed to extract templates: %w", err)
}
}
@@ -404,6 +421,32 @@ func copyDir(src, dst string) error {
return nil
}
// extractFS extracts files from an fs.FS to a destination directory
func extractFS(fsys fs.FS, dst string) error {
return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Create destination path
dstPath := filepath.Join(dst, path)
if d.IsDir() {
// Create directory
return os.MkdirAll(dstPath, 0755)
}
// Read file from embedded FS
data, err := fs.ReadFile(fsys, path)
if err != nil {
return err
}
// Write file to destination
return os.WriteFile(dstPath, data, 0644)
})
}
// Compile processes gomplate templates into final Kubernetes manifests
func (m *Manager) Compile(instanceName, serviceName string) error {
instanceDir := filepath.Join(m.dataDir, "instances", instanceName)

Some files were not shown because too many files have changed in this diff Show More