Skip to content
Merged
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: 9 additions & 2 deletions docs/reference/blueprint.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,20 @@ sources:
url: github.com/windsorcli/core
ref:
tag: v0.3.0
- name: oci-source
url: oci://ghcr.io/windsorcli/core:v0.3.0
# No ref needed for OCI - version is in the URL
```

| Field | Type | Description |
|--------------|------------|--------------------------------------------------|
| `name` | `string` | Identifies the source. |
| `url` | `string` | The source location. |
| `ref` | `Reference`| Details the branch, tag, or commit to use. |
| `url` | `string` | The source location. Supports Git URLs and OCI URLs (oci://registry/repo:tag). |
| `ref` | `Reference`| Details the branch, tag, or commit to use. Not needed for OCI URLs with embedded tags. |
| `secretName` | `string` | The secret for source access. |

**Note:** For OCI sources, the URL should include the tag/version directly (e.g., `oci://registry.example.com/repo:v1.0.0`). The `ref` field is optional for OCI sources when the tag is specified in the URL.

### Reference
A reference to a specific git state or version

Expand Down Expand Up @@ -184,6 +189,8 @@ sources:
url: github.com/windsorcli/core
ref:
branch: main
- name: oci-source
url: oci://ghcr.io/windsorcli/core:v0.3.0
terraform:
- path: cluster/talos
- path: gitops/flux
Expand Down
127 changes: 105 additions & 22 deletions pkg/blueprint/blueprint_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,27 +230,27 @@ func (b *BaseBlueprintHandler) Install() error {
return fmt.Errorf("failed to create namespace: %w", err)
}

// Apply GitRepository for the main repository
// Apply blueprint repository
if b.blueprint.Repository.Url != "" {
source := blueprintv1alpha1.Source{
Name: b.blueprint.Metadata.Name,
Url: b.blueprint.Repository.Url,
Ref: b.blueprint.Repository.Ref,
SecretName: b.blueprint.Repository.SecretName,
}
if err := b.applyGitRepository(source, constants.DEFAULT_FLUX_SYSTEM_NAMESPACE); err != nil {
if err := b.applySourceRepository(source, constants.DEFAULT_FLUX_SYSTEM_NAMESPACE); err != nil {
spin.Stop()
fmt.Fprintf(os.Stderr, "✗%s - \033[31mFailed\033[0m\n", spin.Suffix)
return fmt.Errorf("failed to apply main repository: %w", err)
return fmt.Errorf("failed to apply blueprint repository: %w", err)
}
}

// Apply GitRepositories for sources
// Apply other sources
for _, source := range b.blueprint.Sources {
if err := b.applyGitRepository(source, constants.DEFAULT_FLUX_SYSTEM_NAMESPACE); err != nil {
if err := b.applySourceRepository(source, constants.DEFAULT_FLUX_SYSTEM_NAMESPACE); err != nil {
spin.Stop()
fmt.Fprintf(os.Stderr, "✗%s - \033[31mFailed\033[0m\n", spin.Suffix)
return fmt.Errorf("failed to apply source repository %s: %w", source.Name, err)
return fmt.Errorf("failed to apply source %s: %w", source.Name, err)
}
}

Expand Down Expand Up @@ -505,15 +505,11 @@ func (b *BaseBlueprintHandler) ProcessContextTemplates(contextName string, reset
return fmt.Errorf("error creating context directory: %w", err)
}

// Check for --blueprint flag first (highest priority)
// If --blueprint is specified, use embedded templates and ignore _template directory
blueprintValue := b.configHandler.GetString("blueprint")
if blueprintValue != "" {
return b.generateDefaultBlueprint(contextDir, contextName, resetMode)
}

// Check for _template directory (second priority)
// Only used if --blueprint flag is not specified
templateDir := filepath.Join(projectRoot, "contexts", "_template")
if _, err := b.shims.Stat(templateDir); err == nil {
return b.processTemplateDirectory(templateDir, contextDir, contextName, resetMode)
Expand All @@ -537,11 +533,13 @@ func (b *BaseBlueprintHandler) processTemplateDirectory(templateDir, contextDir,
}
}

// No blueprint template found, generate default
return b.generateDefaultBlueprint(contextDir, contextName, resetMode)
}

// processJsonnetTemplate processes a single blueprint jsonnet template
// processJsonnetTemplate reads and evaluates a jsonnet template file to generate blueprint configuration.
// It loads the template file, marshals the current context configuration to JSON for use as template data,
// evaluates the jsonnet template with the context data injected via ExtCode, and processes the resulting
// blueprint content through the standard blueprint template processing pipeline.
func (b *BaseBlueprintHandler) processJsonnetTemplate(templateFile, contextDir, contextName string, resetMode bool) error {
jsonnetData, err := b.shims.ReadFile(templateFile)
if err != nil {
Expand All @@ -565,9 +563,6 @@ func (b *BaseBlueprintHandler) processJsonnetTemplate(templateFile, contextDir,
return fmt.Errorf("error marshalling context map to JSON: %w", err)
}

// Use ExtCode to make context available via std.extVar("context")
// Templates must include: local context = std.extVar("context");
// This follows standard jsonnet patterns and is explicit and debuggable
vm := b.shims.NewJsonnetVM()
vm.ExtCode("context", string(contextJSON))
evaluatedContent, err := vm.EvaluateAnonymousSnippet(templateFile, string(jsonnetData))
Expand Down Expand Up @@ -655,32 +650,30 @@ func (b *BaseBlueprintHandler) generateDefaultBlueprint(contextDir, contextName
return nil
}

// processBlueprintTemplate validates blueprint template output against the Blueprint schema
// and writes it as a properly formatted blueprint.yaml file
// processBlueprintTemplate validates blueprint template output against the Blueprint schema,
// applies context-specific metadata overrides, and writes the result as a properly formatted
// blueprint.yaml file. It ensures template content conforms to the Blueprint schema before
// persisting to disk and automatically sets the blueprint name and description based on context.
func (b *BaseBlueprintHandler) processBlueprintTemplate(outputPath, content, contextName string, resetMode bool) error {
if !resetMode {
if _, err := b.shims.Stat(outputPath); err == nil {
return nil
}
}

// Validate blueprint content against schema
var testBlueprint blueprintv1alpha1.Blueprint
if err := b.processBlueprintData([]byte(content), &testBlueprint); err != nil {
return fmt.Errorf("error validating blueprint template: %w", err)
}

// Override metadata with context-specific values
testBlueprint.Metadata.Name = contextName
testBlueprint.Metadata.Description = fmt.Sprintf("This blueprint outlines resources in the %s context", contextName)

// Convert JSON content to YAML format
yamlData, err := b.shims.YamlMarshal(testBlueprint)
if err != nil {
return fmt.Errorf("error converting blueprint to YAML: %w", err)
}

// Write validated blueprint content as YAML
if err := b.shims.WriteFile(outputPath, yamlData, 0644); err != nil {
return fmt.Errorf("error writing blueprint file: %w", err)
}
Expand Down Expand Up @@ -894,6 +887,14 @@ func (b *BaseBlueprintHandler) loadPlatformTemplate(platform string) ([]byte, er
}
}

// applySourceRepository routes to the appropriate source handler based on URL type
func (b *BaseBlueprintHandler) applySourceRepository(source blueprintv1alpha1.Source, namespace string) error {
if strings.HasPrefix(source.Url, "oci://") {
return b.applyOCIRepository(source, namespace)
}
return b.applyGitRepository(source, namespace)
}

// applyGitRepository creates or updates a GitRepository resource in the cluster. It normalizes
// the repository URL format, configures standard intervals and timeouts, and handles secret
// references for private repositories.
Expand Down Expand Up @@ -938,6 +939,66 @@ func (b *BaseBlueprintHandler) applyGitRepository(source blueprintv1alpha1.Sourc
return b.kubernetesManager.ApplyGitRepository(gitRepo)
}

// applyOCIRepository creates or updates an OCIRepository resource in the cluster. It handles
// OCI URL parsing, configures standard intervals and timeouts, and handles secret references
// for private registries. The OCI URL should include the tag/version (e.g., oci://registry/repo:tag).
func (b *BaseBlueprintHandler) applyOCIRepository(source blueprintv1alpha1.Source, namespace string) error {
ociURL := source.Url
var ref *sourcev1.OCIRepositoryRef

if lastColon := strings.LastIndex(ociURL, ":"); lastColon > len("oci://") {
if tagPart := ociURL[lastColon+1:]; tagPart != "" && !strings.Contains(tagPart, "/") {
ociURL = ociURL[:lastColon]
ref = &sourcev1.OCIRepositoryRef{
Tag: tagPart,
}
}
}

if ref == nil && (source.Ref.Tag != "" || source.Ref.SemVer != "" || source.Ref.Commit != "") {
ref = &sourcev1.OCIRepositoryRef{
Tag: source.Ref.Tag,
SemVer: source.Ref.SemVer,
Digest: source.Ref.Commit,
}
}

if ref == nil {
ref = &sourcev1.OCIRepositoryRef{
Tag: "latest",
}
}

ociRepo := &sourcev1.OCIRepository{
TypeMeta: metav1.TypeMeta{
Kind: "OCIRepository",
APIVersion: "source.toolkit.fluxcd.io/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: source.Name,
Namespace: namespace,
},
Spec: sourcev1.OCIRepositorySpec{
URL: ociURL,
Interval: metav1.Duration{
Duration: constants.DEFAULT_FLUX_SOURCE_INTERVAL,
},
Timeout: &metav1.Duration{
Duration: constants.DEFAULT_FLUX_SOURCE_TIMEOUT,
},
Reference: ref,
},
}

if source.SecretName != "" {
ociRepo.Spec.SecretRef = &meta.LocalObjectReference{
Name: source.SecretName,
}
}

return b.kubernetesManager.ApplyOCIRepository(ociRepo)
}

// applyConfigMap creates or updates a ConfigMap in the cluster containing context-specific
// configuration values used by the blueprint's resources, such as domain names, IP ranges,
// and volume paths.
Expand Down Expand Up @@ -1085,6 +1146,7 @@ func (b *BaseBlueprintHandler) deleteNamespace(name string) error {
// It handles conversion of dependsOn, patches, and postBuild configurations
// It maps blueprint fields to their Flux kustomization equivalents
// It maintains namespace context and preserves all configuration options
// It automatically detects OCI sources and sets the appropriate SourceRef kind
func (b *BaseBlueprintHandler) toKubernetesKustomization(k blueprintv1alpha1.Kustomization, namespace string) kustomizev1.Kustomization {
dependsOn := make([]meta.NamespacedObjectReference, len(k.DependsOn))
for i, dep := range k.DependsOn {
Expand Down Expand Up @@ -1130,6 +1192,11 @@ func (b *BaseBlueprintHandler) toKubernetesKustomization(k blueprintv1alpha1.Kus
prune = *k.Prune
}

sourceKind := "GitRepository"
if b.isOCISource(k.Source) {
sourceKind = "OCIRepository"
}

return kustomizev1.Kustomization{
TypeMeta: metav1.TypeMeta{
Kind: "Kustomization",
Expand All @@ -1141,7 +1208,7 @@ func (b *BaseBlueprintHandler) toKubernetesKustomization(k blueprintv1alpha1.Kus
},
Spec: kustomizev1.KustomizationSpec{
SourceRef: kustomizev1.CrossNamespaceSourceReference{
Kind: "GitRepository",
Kind: sourceKind,
Name: k.Source,
},
Path: k.Path,
Expand All @@ -1158,3 +1225,19 @@ func (b *BaseBlueprintHandler) toKubernetesKustomization(k blueprintv1alpha1.Kus
},
}
}

// isOCISource determines whether a given source name corresponds to an OCI repository
// source by examining the URL prefix of the blueprint's main repository and any additional sources
func (b *BaseBlueprintHandler) isOCISource(sourceName string) bool {
if sourceName == b.blueprint.Metadata.Name && strings.HasPrefix(b.blueprint.Repository.Url, "oci://") {
return true
}

for _, source := range b.blueprint.Sources {
if source.Name == sourceName && strings.HasPrefix(source.Url, "oci://") {
return true
}
}

return false
}
Loading
Loading