bba1e8eba7
schemas / check-release (pull_request) Failing after 1m31s
schemas / vulnerabilities (pull_request) Successful in 2m19s
schemas / check (pull_request) Successful in 4m28s
schemas / build (pull_request) Has been skipped
schemas / deploy-prod (pull_request) Has been skipped
pre-commit / pre-commit (pull_request) Successful in 6m3s
- Update git remote to git.unbound.se - Add Gitea workflows: ci.yaml, pre-commit.yaml, release.yaml, goreleaser.yaml - Delete .gitlab-ci.yml - Update Go module path to gitea.unbound.se/unboundsoftware/schemas - Update all imports to new module path - Update Docker registry to oci.unbound.se - Update .goreleaser.yml for Gitea releases with internal cluster URL - Remove GitLab CI linter from pre-commit config - Use shared release workflow with tag_only for versioning Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
126 lines
3.6 KiB
Go
126 lines
3.6 KiB
Go
package graph
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
|
)
|
|
|
|
// CommandExecutor is an interface for executing external commands
|
|
// This allows for mocking in tests
|
|
type CommandExecutor interface {
|
|
Execute(name string, args ...string) ([]byte, error)
|
|
}
|
|
|
|
// DefaultCommandExecutor implements CommandExecutor using os/exec
|
|
type DefaultCommandExecutor struct{}
|
|
|
|
// Execute runs a command and returns its combined output
|
|
func (e *DefaultCommandExecutor) Execute(name string, args ...string) ([]byte, error) {
|
|
cmd := exec.Command(name, args...)
|
|
return cmd.CombinedOutput()
|
|
}
|
|
|
|
// GenerateCosmoRouterConfig generates a Cosmo Router execution config from subgraphs
|
|
// using the official wgc CLI tool via npx
|
|
func GenerateCosmoRouterConfig(subGraphs []*model.SubGraph) (string, error) {
|
|
return GenerateCosmoRouterConfigWithExecutor(subGraphs, &DefaultCommandExecutor{})
|
|
}
|
|
|
|
// GenerateCosmoRouterConfigWithExecutor generates a Cosmo Router execution config from subgraphs
|
|
// using the provided command executor (useful for testing)
|
|
func GenerateCosmoRouterConfigWithExecutor(subGraphs []*model.SubGraph, executor CommandExecutor) (string, error) {
|
|
if len(subGraphs) == 0 {
|
|
return "", fmt.Errorf("no subgraphs provided")
|
|
}
|
|
|
|
// Create a temporary directory for composition
|
|
tmpDir, err := os.MkdirTemp("", "cosmo-compose-*")
|
|
if err != nil {
|
|
return "", fmt.Errorf("create temp dir: %w", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Write each subgraph SDL to a file
|
|
type SubgraphConfig struct {
|
|
Name string `yaml:"name"`
|
|
RoutingURL string `yaml:"routing_url,omitempty"`
|
|
Schema map[string]string `yaml:"schema"`
|
|
Subscription map[string]interface{} `yaml:"subscription,omitempty"`
|
|
}
|
|
|
|
type InputConfig struct {
|
|
Version int `yaml:"version"`
|
|
Subgraphs []SubgraphConfig `yaml:"subgraphs"`
|
|
}
|
|
|
|
inputConfig := InputConfig{
|
|
Version: 1,
|
|
Subgraphs: make([]SubgraphConfig, 0, len(subGraphs)),
|
|
}
|
|
|
|
for _, sg := range subGraphs {
|
|
// Write SDL to a temp file
|
|
schemaFile := filepath.Join(tmpDir, fmt.Sprintf("%s.graphql", sg.Service))
|
|
if err := os.WriteFile(schemaFile, []byte(sg.Sdl), 0o644); err != nil {
|
|
return "", fmt.Errorf("write schema file for %s: %w", sg.Service, err)
|
|
}
|
|
|
|
subgraphCfg := SubgraphConfig{
|
|
Name: sg.Service,
|
|
Schema: map[string]string{
|
|
"file": schemaFile,
|
|
},
|
|
}
|
|
|
|
if sg.URL != nil {
|
|
subgraphCfg.RoutingURL = *sg.URL
|
|
}
|
|
|
|
if sg.WsURL != nil {
|
|
subgraphCfg.Subscription = map[string]interface{}{
|
|
"url": *sg.WsURL,
|
|
"protocol": "ws",
|
|
"websocket_subprotocol": "graphql-ws",
|
|
}
|
|
}
|
|
|
|
inputConfig.Subgraphs = append(inputConfig.Subgraphs, subgraphCfg)
|
|
}
|
|
|
|
// Write input config YAML
|
|
inputFile := filepath.Join(tmpDir, "input.yaml")
|
|
inputYAML, err := yaml.Marshal(inputConfig)
|
|
if err != nil {
|
|
return "", fmt.Errorf("marshal input config: %w", err)
|
|
}
|
|
if err := os.WriteFile(inputFile, inputYAML, 0o644); err != nil {
|
|
return "", fmt.Errorf("write input config: %w", err)
|
|
}
|
|
|
|
// Execute wgc router compose
|
|
// wgc is installed globally in the Docker image
|
|
outputFile := filepath.Join(tmpDir, "config.json")
|
|
output, err := executor.Execute("wgc", "router", "compose",
|
|
"--input", inputFile,
|
|
"--out", outputFile,
|
|
"--suppress-warnings",
|
|
)
|
|
if err != nil {
|
|
return "", fmt.Errorf("wgc router compose failed: %w\nOutput: %s", err, string(output))
|
|
}
|
|
|
|
// Read the generated config
|
|
configJSON, err := os.ReadFile(outputFile)
|
|
if err != nil {
|
|
return "", fmt.Errorf("read output config: %w", err)
|
|
}
|
|
|
|
return string(configJSON), nil
|
|
}
|