diff --git a/cmd/init.go b/cmd/init.go index d37a67cbb..a7b917bfb 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -140,6 +140,34 @@ var initCmd = &cobra.Command{ } } + // Set platform-specific configurations + if initPlatform != "" { + switch initPlatform { + case "aws": + if err := configHandler.SetContextValue("aws.enabled", true); err != nil { + return fmt.Errorf("Error setting aws.enabled: %w", err) + } + if err := configHandler.SetContextValue("cluster.driver", "eks"); err != nil { + return fmt.Errorf("Error setting cluster.driver: %w", err) + } + case "azure": + if err := configHandler.SetContextValue("azure.enabled", true); err != nil { + return fmt.Errorf("Error setting azure.enabled: %w", err) + } + if err := configHandler.SetContextValue("cluster.driver", "aks"); err != nil { + return fmt.Errorf("Error setting cluster.driver: %w", err) + } + case "metal": + if err := configHandler.SetContextValue("cluster.driver", "talos"); err != nil { + return fmt.Errorf("Error setting cluster.driver: %w", err) + } + case "local": + if err := configHandler.SetContextValue("cluster.driver", "talos"); err != nil { + return fmt.Errorf("Error setting cluster.driver: %w", err) + } + } + } + // Set the vm driver only if it's configured and not overridden by --set flag if vmDriverConfig != "" && configHandler.GetString("vm.driver") == "" { if err := configHandler.SetContextValue("vm.driver", vmDriverConfig); err != nil { diff --git a/cmd/init_test.go b/cmd/init_test.go index fc60cac1a..b2b71956f 100644 --- a/cmd/init_test.go +++ b/cmd/init_test.go @@ -836,3 +836,102 @@ func TestInitCmd(t *testing.T) { } }) } + +type platformTest struct { + name string + flag string + enabledKey string + enabledValue bool + driverKey string + driverExpected string +} + +func TestInitCmd_PlatformFlag(t *testing.T) { + platforms := []platformTest{ + { + name: "aws", + flag: "aws", + enabledKey: "aws.enabled", + enabledValue: true, + driverKey: "cluster.driver", + driverExpected: "eks", + }, + { + name: "azure", + flag: "azure", + enabledKey: "azure.enabled", + enabledValue: true, + driverKey: "cluster.driver", + driverExpected: "aks", + }, + { + name: "metal", + flag: "metal", + enabledKey: "", + enabledValue: false, + driverKey: "cluster.driver", + driverExpected: "talos", + }, + { + name: "local", + flag: "local", + enabledKey: "", + enabledValue: false, + driverKey: "cluster.driver", + driverExpected: "talos", + }, + } + + for _, tc := range platforms { + t.Run(tc.name, func(t *testing.T) { + // Use a real map-backed mock config handler + store := make(map[string]interface{}) + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.SetContextValueFunc = func(key string, value any) error { + store[key] = value + return nil + } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if v, ok := store[key]; ok { + if s, ok := v.(string); ok { + return s + } + } + if len(defaultValue) > 0 { + return defaultValue[0] + } + return "" + } + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if v, ok := store[key]; ok { + if b, ok := v.(bool); ok { + return b + } + } + if len(defaultValue) > 0 { + return defaultValue[0] + } + return false + } + + mocks := setupInitMocks(t, &SetupOptions{ConfigHandler: mockConfigHandler}) + rootCmd.ResetFlags() + initCmd.ResetFlags() + initCmd.Flags().StringVar(&initPlatform, "platform", "", "Specify the platform to use [local|metal]") + + rootCmd.SetArgs([]string{"init", "--platform", tc.flag}) + err := Execute(mocks.Controller) + if err != nil { + t.Fatalf("Expected success, got error: %v", err) + } + if tc.enabledKey != "" { + if !mockConfigHandler.GetBool(tc.enabledKey) { + t.Errorf("Expected %s to be true", tc.enabledKey) + } + } + if got := mockConfigHandler.GetString(tc.driverKey); got != tc.driverExpected { + t.Errorf("Expected %s to be %q, got %q", tc.driverKey, tc.driverExpected, got) + } + }) + } +} diff --git a/pkg/blueprint/blueprint_handler.go b/pkg/blueprint/blueprint_handler.go index 1b092d8fa..10df50cb7 100644 --- a/pkg/blueprint/blueprint_handler.go +++ b/pkg/blueprint/blueprint_handler.go @@ -63,6 +63,18 @@ type BlueprintHandler interface { //go:embed templates/default.jsonnet var defaultJsonnetTemplate string +//go:embed templates/local.jsonnet +var localJsonnetTemplate string + +//go:embed templates/metal.jsonnet +var metalJsonnetTemplate string + +//go:embed templates/aws.jsonnet +var awsJsonnetTemplate string + +//go:embed templates/azure.jsonnet +var azureJsonnetTemplate string + type BaseBlueprintHandler struct { BlueprintHandler injector di.Injector @@ -134,13 +146,34 @@ func (b *BaseBlueprintHandler) LoadConfig(path ...string) error { basePath = path[0] } - jsonnetData, jsonnetErr := b.loadFileData(basePath + ".jsonnet") - yamlData, yamlErr := b.loadFileData(basePath + ".yaml") - if jsonnetErr != nil { - return jsonnetErr + // Get platform from context + platform := "" + if b.configHandler.GetConfig().Cluster != nil && b.configHandler.GetConfig().Cluster.Platform != nil { + platform = *b.configHandler.GetConfig().Cluster.Platform + } + + // Try to load platform-specific template first + platformData, err := b.loadPlatformTemplate(platform) + if err != nil { + return fmt.Errorf("error loading platform template: %w", err) } - if yamlErr != nil && !os.IsNotExist(yamlErr) { - return yamlErr + + var yamlData []byte + // If no platform template, fall back to default + if len(platformData) == 0 { + jsonnetData, jsonnetErr := b.loadFileData(basePath + ".jsonnet") + var yamlErr error + yamlData, yamlErr = b.loadFileData(basePath + ".yaml") + if jsonnetErr != nil { + return jsonnetErr + } + if yamlErr != nil && !os.IsNotExist(yamlErr) { + return yamlErr + } + + if len(jsonnetData) > 0 { + platformData = jsonnetData + } } config := b.configHandler.GetConfig() @@ -168,8 +201,8 @@ func (b *BaseBlueprintHandler) LoadConfig(path ...string) error { vm := b.shims.NewJsonnetVM() vm.ExtCode("context", string(contextJSON)) - if len(jsonnetData) > 0 { - evaluatedJsonnet, err = vm.EvaluateAnonymousSnippet("blueprint.jsonnet", string(jsonnetData)) + if len(platformData) > 0 { + evaluatedJsonnet, err = vm.EvaluateAnonymousSnippet("blueprint.jsonnet", string(platformData)) if err != nil { return fmt.Errorf("error generating blueprint from jsonnet: %w", err) } @@ -221,6 +254,10 @@ func (b *BaseBlueprintHandler) WriteConfig(path ...string) error { return fmt.Errorf("error creating directory: %w", err) } + if _, err := b.shims.Stat(finalPath); err == nil { + return nil + } + fullBlueprint := b.blueprint.DeepCopy() for i := range fullBlueprint.TerraformComponents { @@ -643,6 +680,26 @@ func (b *BaseBlueprintHandler) loadFileData(path string) ([]byte, error) { return nil, nil } +// loadPlatformTemplate loads a platform-specific template if one exists +func (b *BaseBlueprintHandler) loadPlatformTemplate(platform string) ([]byte, error) { + if platform == "" { + return nil, nil + } + + switch platform { + case "local": + return []byte(localJsonnetTemplate), nil + case "metal": + return []byte(metalJsonnetTemplate), nil + case "aws": + return []byte(awsJsonnetTemplate), nil + case "azure": + return []byte(azureJsonnetTemplate), nil + default: + return nil, nil + } +} + // yamlMarshalWithDefinedPaths marshals data to YAML format while ensuring all parent paths are defined. // It handles various Go types including structs, maps, slices, and primitive types, preserving YAML // tags and properly representing nil values. diff --git a/pkg/blueprint/blueprint_handler_test.go b/pkg/blueprint/blueprint_handler_test.go index 6420bc178..d2d1977c5 100644 --- a/pkg/blueprint/blueprint_handler_test.go +++ b/pkg/blueprint/blueprint_handler_test.go @@ -1115,12 +1115,15 @@ func TestBlueprintHandler_WriteConfig(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given a blueprint handler with metadata handler, mocks := setup(t) + // Patch Stat to simulate file does not exist + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } expectedMetadata := blueprintv1alpha1.Metadata{ Name: "test-blueprint", Description: "A test blueprint", Authors: []string{"John Doe"}, } - handler.SetMetadata(expectedMetadata) // And a mock file system that captures written data @@ -1164,6 +1167,10 @@ func TestBlueprintHandler_WriteConfig(t *testing.T) { t.Run("WriteNoPath", func(t *testing.T) { // Given a blueprint handler with metadata handler, mocks := setup(t) + // Patch Stat to simulate file does not exist + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } expectedMetadata := blueprintv1alpha1.Metadata{ Name: "test-blueprint", Description: "A test blueprint", @@ -1260,6 +1267,10 @@ func TestBlueprintHandler_WriteConfig(t *testing.T) { t.Run("ErrorMarshallingYaml", func(t *testing.T) { // Given a blueprint handler handler, mocks := setup(t) + // Patch Stat to simulate file does not exist + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } // And a mock yaml marshaller that returns an error mocks.Shims.YamlMarshalNonNull = func(in any) ([]byte, error) { @@ -1281,6 +1292,10 @@ func TestBlueprintHandler_WriteConfig(t *testing.T) { t.Run("ErrorWritingFile", func(t *testing.T) { // Given a blueprint handler handler, mocks := setup(t) + // Patch Stat to simulate file does not exist + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } // And a mock file system that fails to write files mocks.Shims.WriteFile = func(name string, data []byte, perm fs.FileMode) error { @@ -1302,6 +1317,10 @@ func TestBlueprintHandler_WriteConfig(t *testing.T) { t.Run("CleanupEmptyPostBuild", func(t *testing.T) { // Given a blueprint handler with kustomizations containing empty PostBuild handler, mocks := setup(t) + // Patch Stat to simulate file does not exist + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } emptyPostBuildKustomizations := []blueprintv1alpha1.Kustomization{ { Name: "kustomization-empty-postbuild", @@ -1370,6 +1389,10 @@ func TestBlueprintHandler_WriteConfig(t *testing.T) { t.Run("ClearTerraformComponentsVariablesAndValues", func(t *testing.T) { // Given a blueprint handler with terraform components containing variables and values handler, mocks := setup(t) + // Patch Stat to simulate file does not exist + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } terraformComponents := []blueprintv1alpha1.TerraformComponent{ { Source: "source1", @@ -4089,3 +4112,55 @@ func TestBaseBlueprintHandler_WaitForKustomizations(t *testing.T) { } }) } + +func TestBaseBlueprintHandler_loadPlatformTemplate(t *testing.T) { + t.Run("ValidPlatforms", func(t *testing.T) { + // Given a BaseBlueprintHandler + handler := &BaseBlueprintHandler{} + + // When loading templates for valid platforms + platforms := []string{"local", "metal", "aws", "azure"} + for _, platform := range platforms { + // Then the template should be loaded successfully + template, err := handler.loadPlatformTemplate(platform) + if err != nil { + t.Errorf("Expected no error for platform %s, got: %v", platform, err) + } + if len(template) == 0 { + t.Errorf("Expected non-empty template for platform %s", platform) + } + } + }) + + t.Run("InvalidPlatform", func(t *testing.T) { + // Given a BaseBlueprintHandler + handler := &BaseBlueprintHandler{} + + // When loading template for invalid platform + template, err := handler.loadPlatformTemplate("invalid-platform") + + // Then no error should occur but template should be empty + if err != nil { + t.Errorf("Expected no error for invalid platform, got: %v", err) + } + if len(template) != 0 { + t.Errorf("Expected empty template for invalid platform, got length: %d", len(template)) + } + }) + + t.Run("EmptyPlatform", func(t *testing.T) { + // Given a BaseBlueprintHandler + handler := &BaseBlueprintHandler{} + + // When loading template with empty platform + template, err := handler.loadPlatformTemplate("") + + // Then no error should occur and template should be empty + if err != nil { + t.Errorf("Expected no error for empty platform, got: %v", err) + } + if len(template) != 0 { + t.Errorf("Expected empty template for empty platform, got length: %d", len(template)) + } + }) +} diff --git a/pkg/blueprint/templates/aws.jsonnet b/pkg/blueprint/templates/aws.jsonnet new file mode 100644 index 000000000..c77149599 --- /dev/null +++ b/pkg/blueprint/templates/aws.jsonnet @@ -0,0 +1,157 @@ +local context = std.extVar("context"); + +// Repository configuration +local repositoryConfig = { + url: "", + ref: { + branch: "main", + }, + secretName: "flux-system", +}; + +// Terraform configuration +local terraformConfig = [ + { + path: "network/aws-vpc", + source: "core", + }, + { + path: "cluster/aws-eks", + source: "core", + }, + { + path: "gitops/flux", + source: "core", + destroy: false, + } +]; + +// Determine the blueprint, defaulting to an empty string if not defined +local blueprint = if std.objectHas(context, "blueprint") then context.blueprint else ""; + +// Kustomize configuration +local kustomizeConfig = if blueprint == "full" then [ + { + name: "telemetry-base", + source: "core", + path: "telemetry/base", + components: [ + "prometheus", + "prometheus/flux" + ], + }, + { + name: "telemetry-resources", + source: "core", + path: "telemetry/resources", + dependsOn: [ + "telemetry-base" + ], + components: [ + "metrics-server", + "prometheus", + "prometheus/flux" + ], + }, + { + name: "policy-base", + source: "core", + path: "policy/base", + components: [ + "kyverno" + ], + }, + { + name: "policy-resources", + source: "core", + path: "policy/resources", + dependsOn: [ + "policy-base" + ], + }, + { + name: "ingress-base", + source: "core", + path: "ingress/base", + dependsOn: [ + "pki-resources" + ], + force: true, + components: [ + "nginx", + "nginx/flux-webhook", + "nginx/web" + ], + }, + { + name: "pki-base", + source: "core", + path: "pki/base", + dependsOn: [ + "policy-resources" + ], + force: true, + components: [ + "cert-manager", + "trust-manager" + ], + }, + { + name: "pki-resources", + source: "core", + path: "pki/resources", + dependsOn: [ + "pki-base" + ], + force: true, + components: [ + "private-issuer/ca", + "public-issuer/selfsigned" + ], + }, + { + name: "observability", + source: "core", + path: "observability", + dependsOn: [ + "ingress-base" + ], + components: [ + "grafana", + "grafana/ingress", + "grafana/prometheus", + "grafana/node", + "grafana/kubernetes", + "grafana/flux" + ], + } +] else []; + +// Blueprint metadata +local blueprintMetadata = { + kind: "Blueprint", + apiVersion: "blueprints.windsorcli.dev/v1alpha1", + metadata: { + name: context.name, + description: "This blueprint outlines resources in the " + context.name + " context", + }, +}; + +// Source configuration +local sourceConfig = [ + { + name: "core", + url: "github.com/windsorcli/core", + ref: { + branch: "main", + }, + }, +]; + +// Start of Blueprint +blueprintMetadata + { + repository: repositoryConfig, + sources: sourceConfig, + terraform: terraformConfig, + kustomize: kustomizeConfig, +} diff --git a/pkg/blueprint/templates/azure.jsonnet b/pkg/blueprint/templates/azure.jsonnet new file mode 100644 index 000000000..212d9e3fe --- /dev/null +++ b/pkg/blueprint/templates/azure.jsonnet @@ -0,0 +1,181 @@ +local context = std.extVar("context"); + +// Repository configuration +local repositoryConfig = { + url: "", + ref: { + branch: "main", + }, + secretName: "flux-system", +}; + +// Terraform configuration +local terraformConfig = [ + { + path: "network/azure-vnet", + source: "core", + }, + { + path: "cluster/azure-aks", + source: "core", + }, + { + path: "gitops/flux", + source: "core", + destroy: false, + } +]; + +// Determine the blueprint, defaulting to an empty string if not defined +local blueprint = if std.objectHas(context, "blueprint") then context.blueprint else ""; + +// Kustomize configuration +local kustomizeConfig = if blueprint == "full" then [ + { + name: "telemetry-base", + source: "core", + path: "telemetry/base", + components: [ + "prometheus", + "prometheus/flux" + ], + }, + { + name: "telemetry-resources", + source: "core", + path: "telemetry/resources", + dependsOn: [ + "telemetry-base" + ], + components: [ + "prometheus", + "prometheus/flux" + ], + }, + { + name: "policy-base", + source: "core", + path: "policy/base", + components: [ + "kyverno" + ], + }, + { + name: "policy-resources", + source: "core", + path: "policy/resources", + dependsOn: [ + "policy-base" + ], + }, + { + name: "ingress-base", + source: "core", + path: "ingress/base", + dependsOn: [ + "pki-resources" + ], + force: true, + components: [ + "nginx", + "nginx/flux-webhook", + "nginx/web" + ], + }, + { + name: "pki-base", + source: "core", + path: "pki/base", + dependsOn: [ + "policy-resources" + ], + force: true, + components: [ + "cert-manager", + "trust-manager" + ], + }, + { + name: "pki-resources", + source: "core", + path: "pki/resources", + dependsOn: [ + "pki-base" + ], + force: true, + components: [ + "private-issuer/ca", + "public-issuer/selfsigned" + ], + }, + { + name: "dns", + source: "core", + path: "dns", + dependsOn: [ + "pki-base" + ], + force: true, + components: [ + "external-dns", + "external-dns/ingress" + ], + }, + { + name: "gitops", + source: "core", + path: "gitops/flux", + dependsOn: [ + "ingress-base" + ], + force: true, + components: [ + "webhook" + ], + }, + { + name: "observability", + source: "core", + path: "observability", + dependsOn: [ + "ingress-base" + ], + components: [ + "grafana", + "grafana/ingress", + "grafana/prometheus", + "grafana/node", + "grafana/kubernetes", + "grafana/flux" + ], + } +] else []; + +// Blueprint metadata +local blueprintMetadata = { + kind: "Blueprint", + apiVersion: "blueprints.windsorcli.dev/v1alpha1", + metadata: { + name: context.name, + description: "This blueprint outlines resources in the " + context.name + " context", + }, +}; + +// Source configuration +local sourceConfig = [ + { + name: "core", + url: "github.com/windsorcli/core", + ref: { + branch: "main", + }, + }, +]; + +// Start of Blueprint +blueprintMetadata + { + repository: repositoryConfig, + sources: sourceConfig, + terraform: terraformConfig, + kustomize: kustomizeConfig, +} diff --git a/pkg/blueprint/templates/default.jsonnet b/pkg/blueprint/templates/default.jsonnet index 416f195df..c6287b2c1 100644 --- a/pkg/blueprint/templates/default.jsonnet +++ b/pkg/blueprint/templates/default.jsonnet @@ -1,414 +1,28 @@ local context = std.extVar("context"); -// Determine the platform, defaulting to an empty string if not defined -local platform = if std.objectHas(context, "cluster") && std.objectHas(context.cluster, "platform") && context.cluster.platform != null then context.cluster.platform else ""; - -// Determine the vmDriver, defaulting to an empty string if not defined -local vmDriver = if std.objectHas(context, "vm") && std.objectHas(context.vm, "driver") - then context.vm.driver - else ""; - -// Safely access control plane nodes from the context, defaulting to an empty array if not present -local cpNodes = if std.objectHas(context, "cluster") && std.objectHas(context.cluster, "controlplanes") && std.objectHas(context.cluster.controlplanes, "nodes") - then std.objectValues(context.cluster.controlplanes.nodes) - else []; - -// Select the first node or default to null if no nodes are present -local firstNode = if std.length(cpNodes) > 0 then cpNodes[0] else null; - -// Extract baseUrl from endpoint -local extractBaseUrl(endpoint) = - if endpoint == "" then "" else - local parts = std.split(endpoint, "://"); - if std.length(parts) > 1 then - local hostParts = std.split(parts[1], ":"); - hostParts[0] - else - local hostParts = std.split(endpoint, ":"); - hostParts[0]; - -// Determine the endpoint, using cluster.endpoint if available, otherwise falling back to firstNode -local endpoint = if std.objectHas(context.cluster, "endpoint") then context.cluster.endpoint else if firstNode != null then firstNode.endpoint else ""; -local baseUrl = extractBaseUrl(endpoint); - -// Build certSANs list -local certSANs = ["localhost", baseUrl] + (if std.objectHas(context.cluster, "controlplanes") && std.objectHas(context.cluster.controlplanes, "nodes") && std.length(std.objectValues(context.cluster.controlplanes.nodes)) > 0 then - local firstNode = std.objectValues(context.cluster.controlplanes.nodes)[0]; - local hostname = firstNode.hostname; - local domain = if std.objectHas(context, "dns") && std.objectHas(context.dns, "domain") then context.dns.domain else ""; - [hostname] + (if domain != "" then [hostname + "." + domain] else []) -else []); - -// Build the mirrors dynamically, only if registries are defined -local registryMirrors = if std.objectHas(context, "docker") && std.objectHas(context.docker, "registries") then - std.foldl( - function(acc, key) - local registryInfo = context.docker.registries[key]; - local localOverride = if std.objectHas(registryInfo, "local") - then - local parts = std.split(registryInfo["local"], "//"); - if std.length(parts) > 1 then parts[1] else registryInfo["local"] - else ""; - - if std.objectHas(registryInfo, "hostname") && registryInfo.hostname != "" then - acc + { - [(if localOverride != "" then localOverride else key)]: { - endpoints: ["http://" + registryInfo.hostname + ":5000"], - }, - } - else - acc, - std.objectFields(context.docker.registries), - {} - ) -else {}; - -// Blueprint metadata -local blueprintMetadata = { +{ kind: "Blueprint", apiVersion: "blueprints.windsorcli.dev/v1alpha1", metadata: { name: context.name, description: "This blueprint outlines resources in the " + context.name + " context", }, -}; - -// Repository configuration -local repositoryConfig = { - url: if platform != "local" then "" else "http://git.test/git/" + context.projectName, - ref: { - branch: "main", - }, - secretName: "flux-system", -}; - -// Source configuration -local sourceConfig = [ - { - name: "core", - url: "github.com/windsorcli/core", + repository: { + url: "", ref: { - // renovate: datasource=github-branches depName=windsorcli/core branch: "main", }, - }, -]; - -// Terraform configuration -local terraformConfig = if platform == "local" || platform == "metal" then [ - { - path: "cluster/talos", - source: "core", - values: { - // Use the determined endpoint - cluster_endpoint: if endpoint != "" then "https://" + baseUrl + ":6443" else "", - cluster_name: "talos", - - // Create a list of control plane nodes - controlplanes: if std.objectHas(context.cluster, "controlplanes") && std.objectHas(context.cluster.controlplanes, "nodes") then - std.map( - function(v) { - endpoint: v.endpoint, - node: v.node, - }, - std.objectValues(context.cluster.controlplanes.nodes) - ) - else [], - - // Create a list of worker nodes - workers: if std.objectHas(context.cluster, "workers") && std.objectHas(context.cluster.workers, "nodes") then - std.map( - function(v) { - endpoint: v.endpoint, - node: v.node, - }, - std.objectValues(context.cluster.workers.nodes) - ) - else [], - - // Convert common configuration patches to YAML format - common_config_patches: std.manifestYamlDoc( - // We'll build the 'cluster' and 'machine' objects, - // then conditionally add 'registries' if needed - { - cluster: { - apiServer: { - certSANs: certSANs, - }, - extraManifests: [ - // renovate: datasource=github-releases depName=kubelet-serving-cert-approver package=alex1989hu/kubelet-serving-cert-approver - "https://raw.githubusercontent.com/alex1989hu/kubelet-serving-cert-approver/v0.8.7/deploy/standalone-install.yaml", - ], - }, - } - + - // Merge in the base `machine` config - { - machine: { - certSANs: certSANs, - network: if vmDriver == "docker-desktop" then { - interfaces: [ - { - ignore: true, - interface: "eth0", - }, - ], - } else {}, - kubelet: { - extraArgs: { - "rotate-server-certificates": "true", - }, - }, - }, - } - + - // Conditionally add 'machine.registries' only if registryMirrors is non-empty - (if std.length(std.objectFields(registryMirrors)) == 0 then - {} - else - { - machine+: { - registries: { - mirrors: registryMirrors, - }, - }, - }) - ), - worker_config_patches: if std.objectHas(context.cluster, "workers") && std.objectHas(context.cluster.workers, "volumes") && std.length(context.cluster.workers.volumes) > 0 then - std.manifestYamlDoc( - { - machine: { - kubelet: { - extraMounts: std.map( - function(volume) - local parts = std.split(volume, ":"); - { - destination: parts[1], - type: "bind", - source: parts[1], - options: [ - "rbind", - "rw", - ], - }, - context.cluster.workers.volumes - ), - }, - }, - } - ) - else - "", - controlplane_config_patches: if std.objectHas(context.cluster, "controlplanes") && std.objectHas(context.cluster.controlplanes, "volumes") && std.length(context.cluster.controlplanes.volumes) > 0 then - std.manifestYamlDoc( - { - machine: { - kubelet: { - extraMounts: std.map( - function(volume) - local parts = std.split(volume, ":"); - { - destination: parts[1], - type: "bind", - source: parts[1], - options: [ - "rbind", - "rw", - ], - }, - context.cluster.controlplanes.volumes - ), - }, - }, - } - ) - else - "", + secretName: "flux-system", + }, + sources: [ + { + name: "core", + url: "github.com/windsorcli/core", + ref: { + branch: "main", + }, }, - }, - { - path: "gitops/flux", - source: "core", - destroy: false, - values: if platform == "local" then { - git_username: "local", - git_password: "local", - webhook_token: "abcdef123456", - } else {}, - } -] else []; - -// Determine the blueprint, defaulting to an empty string if not defined -local blueprint = if std.objectHas(context, "blueprint") then context.blueprint else ""; - -// Kustomize configuration -local kustomizeConfig = if blueprint == "full" then [ - { - name: "telemetry-base", - source: "core", - path: "telemetry/base", - components: [ - "prometheus", - "prometheus/flux" - ], - }, - { - name: "telemetry-resources", - source: "core", - path: "telemetry/resources", - dependsOn: [ - "telemetry-base" - ], - components: [ - "metrics-server", - "prometheus", - "prometheus/flux" - ], - }, - { - name: "policy-base", - source: "core", - path: "policy/base", - components: [ - "kyverno" - ], - }, - { - name: "policy-resources", - source: "core", - path: "policy/resources", - dependsOn: [ - "policy-base" - ], - }, - { - name: "csi", - source: "core", - path: "csi", - dependsOn: [ - "policy-resources" - ], - force: true, - components: [ - "openebs", - "openebs/dynamic-localpv", - ], - }, -] + (if vmDriver != "docker-desktop" then [ - { - name: "lb-base", - source: "core", - path: "lb/base", - dependsOn: [ - "policy-resources" - ], - force: true, - components: [ - "metallb" - ], - }, - { - name: "lb-resources", - source: "core", - path: "lb/resources", - dependsOn: [ - "lb-base" - ], - force: true, - components: [ - "metallb/layer2" - ], - } -] else []) + [ - { - name: "ingress-base", - source: "core", - path: "ingress/base", - dependsOn: [ - "pki-resources" - ], - force: true, - components: if vmDriver == "docker-desktop" then [ - "nginx", - "nginx/nodeport", - "nginx/coredns", - "nginx/flux-webhook", - "nginx/web" - ] else [ - "nginx", - "nginx/loadbalancer", - "nginx/coredns", - "nginx/flux-webhook", - "nginx/web" - ], - }, - { - name: "pki-base", - source: "core", - path: "pki/base", - dependsOn: [ - "policy-resources" - ], - force: true, - components: [ - "cert-manager", - "trust-manager" - ], - }, - { - name: "pki-resources", - source: "core", - path: "pki/resources", - dependsOn: [ - "pki-base" - ], - force: true, - components: [ - "private-issuer/ca", - "public-issuer/selfsigned" - ], - }, - { - name: "dns", - source: "core", - path: "dns", - dependsOn: [ - "pki-base" - ], - force: true, - components: if vmDriver == "docker-desktop" then [ - "coredns", - "coredns/etcd", - "external-dns", - "external-dns/localhost", - "external-dns/coredns", - "external-dns/ingress" - ] else [ - "coredns", - "coredns/etcd", - "external-dns", - "external-dns/coredns", - "external-dns/ingress" - ], - }, - { - name: "gitops", - source: "core", - path: "gitops/flux", - dependsOn: [ - "ingress-base" - ], - force: true, - components: [ - "webhook" - ], - } -] else []; - -// Start of Blueprint -blueprintMetadata + { - repository: repositoryConfig, - sources: sourceConfig, - terraform: terraformConfig, - kustomize: kustomizeConfig, + ], + terraform: [], + kustomize: [], } diff --git a/pkg/blueprint/templates/local.jsonnet b/pkg/blueprint/templates/local.jsonnet new file mode 100644 index 000000000..fe1b36029 --- /dev/null +++ b/pkg/blueprint/templates/local.jsonnet @@ -0,0 +1,411 @@ +local context = std.extVar("context"); + +// Determine the vmDriver, defaulting to an empty string if not defined +local vmDriver = if std.objectHas(context, "vm") && std.objectHas(context.vm, "driver") + then context.vm.driver + else ""; + +// Safely access control plane nodes from the context, defaulting to an empty array if not present +local cpNodes = if std.objectHas(context, "cluster") && std.objectHas(context.cluster, "controlplanes") && std.objectHas(context.cluster.controlplanes, "nodes") + then std.objectValues(context.cluster.controlplanes.nodes) + else []; + +// Select the first node or default to null if no nodes are present +local firstNode = if std.length(cpNodes) > 0 then cpNodes[0] else null; + +// Extract baseUrl from endpoint +local extractBaseUrl(endpoint) = + if endpoint == "" then "" else + local parts = std.split(endpoint, "://"); + if std.length(parts) > 1 then + local hostParts = std.split(parts[1], ":"); + hostParts[0] + else + local hostParts = std.split(endpoint, ":"); + hostParts[0]; + +// Determine the endpoint, using cluster.endpoint if available, otherwise falling back to firstNode +local endpoint = if std.objectHas(context.cluster, "endpoint") then context.cluster.endpoint else if firstNode != null then firstNode.endpoint else ""; +local baseUrl = extractBaseUrl(endpoint); + +// Build certSANs list +local certSANs = ["localhost", baseUrl] + (if std.objectHas(context.cluster, "controlplanes") && std.objectHas(context.cluster.controlplanes, "nodes") && std.length(std.objectValues(context.cluster.controlplanes.nodes)) > 0 then + local firstNode = std.objectValues(context.cluster.controlplanes.nodes)[0]; + local hostname = firstNode.hostname; + local domain = if std.objectHas(context, "dns") && std.objectHas(context.dns, "domain") then context.dns.domain else ""; + [hostname] + (if domain != "" then [hostname + "." + domain] else []) +else []); + +// Build the mirrors dynamically, only if registries are defined +local registryMirrors = if std.objectHas(context, "docker") && std.objectHas(context.docker, "registries") then + std.foldl( + function(acc, key) + local registryInfo = context.docker.registries[key]; + local localOverride = if std.objectHas(registryInfo, "local") + then + local parts = std.split(registryInfo["local"], "//"); + if std.length(parts) > 1 then parts[1] else registryInfo["local"] + else ""; + + if std.objectHas(registryInfo, "hostname") && registryInfo.hostname != "" then + acc + { + [(if localOverride != "" then localOverride else key)]: { + endpoints: ["http://" + registryInfo.hostname + ":5000"], + }, + } + else + acc, + std.objectFields(context.docker.registries), + {} + ) +else {}; + +// Repository configuration +local repositoryConfig = { + url: "http://git.test/git/" + context.projectName, + ref: { + branch: "main", + }, + secretName: "flux-system", +}; + +// Terraform configuration +local terraformConfig = [ + { + path: "cluster/talos", + source: "core", + values: { + cluster_endpoint: if endpoint != "" then "https://" + baseUrl + ":6443" else "", + cluster_name: "talos", + controlplanes: if std.objectHas(context.cluster, "controlplanes") && std.objectHas(context.cluster.controlplanes, "nodes") then + std.map( + function(v) { + endpoint: v.endpoint, + node: v.node, + }, + std.objectValues(context.cluster.controlplanes.nodes) + ) + else [], + workers: if std.objectHas(context.cluster, "workers") && std.objectHas(context.cluster.workers, "nodes") then + std.map( + function(v) { + endpoint: v.endpoint, + node: v.node, + }, + std.objectValues(context.cluster.workers.nodes) + ) + else [], + common_config_patches: std.manifestYamlDoc( + { + cluster: { + apiServer: { + certSANs: certSANs, + }, + extraManifests: [ + "https://raw.githubusercontent.com/alex1989hu/kubelet-serving-cert-approver/v0.8.7/deploy/standalone-install.yaml", + ], + }, + machine: { + certSANs: certSANs, + network: if vmDriver == "docker-desktop" then { + interfaces: [ + { + ignore: true, + interface: "eth0", + }, + ], + } else {}, + kubelet: { + extraArgs: { + "rotate-server-certificates": "true", + }, + }, + }, + } + + + (if std.length(std.objectFields(registryMirrors)) == 0 then + {} + else + { + machine+: { + registries: { + mirrors: registryMirrors, + }, + }, + }) + ), + worker_config_patches: if std.objectHas(context.cluster, "workers") && std.objectHas(context.cluster.workers, "volumes") && std.length(context.cluster.workers.volumes) > 0 then + std.manifestYamlDoc( + { + machine: { + kubelet: { + extraMounts: std.map( + function(volume) + local parts = std.split(volume, ":"); + { + destination: parts[1], + type: "bind", + source: parts[1], + options: [ + "rbind", + "rw", + ], + }, + context.cluster.workers.volumes + ), + }, + }, + } + ) + else + "", + controlplane_config_patches: if std.objectHas(context.cluster, "controlplanes") && std.objectHas(context.cluster.controlplanes, "volumes") && std.length(context.cluster.controlplanes.volumes) > 0 then + std.manifestYamlDoc( + { + machine: { + kubelet: { + extraMounts: std.map( + function(volume) + local parts = std.split(volume, ":"); + { + destination: parts[1], + type: "bind", + source: parts[1], + options: [ + "rbind", + "rw", + ], + }, + context.cluster.controlplanes.volumes + ), + }, + }, + } + ) + else + "", + }, + }, + { + path: "gitops/flux", + source: "core", + destroy: false, + values: { + git_username: "local", + git_password: "local", + webhook_token: "abcdef123456", + }, + } +]; + +// Determine the blueprint, defaulting to an empty string if not defined +local blueprint = if std.objectHas(context, "blueprint") then context.blueprint else ""; + +// Kustomize configuration +local kustomizeConfig = if blueprint == "full" then [ + { + name: "telemetry-base", + source: "core", + path: "telemetry/base", + components: [ + "prometheus", + "prometheus/flux" + ], + }, + { + name: "telemetry-resources", + source: "core", + path: "telemetry/resources", + dependsOn: [ + "telemetry-base" + ], + components: [ + "metrics-server", + "prometheus", + "prometheus/flux" + ], + }, + { + name: "policy-base", + source: "core", + path: "policy/base", + components: [ + "kyverno" + ], + }, + { + name: "policy-resources", + source: "core", + path: "policy/resources", + dependsOn: [ + "policy-base" + ], + }, + { + name: "csi", + source: "core", + path: "csi", + dependsOn: [ + "policy-resources" + ], + force: true, + components: [ + "openebs", + "openebs/dynamic-localpv", + ], + }, +] + (if vmDriver != "docker-desktop" then [ + { + name: "lb-base", + source: "core", + path: "lb/base", + dependsOn: [ + "policy-resources" + ], + force: true, + components: [ + "metallb" + ], + }, + { + name: "lb-resources", + source: "core", + path: "lb/resources", + dependsOn: [ + "lb-base" + ], + force: true, + components: [ + "metallb/layer2" + ], + } +] else []) + [ + { + name: "ingress-base", + source: "core", + path: "ingress/base", + dependsOn: [ + "pki-resources" + ], + force: true, + components: if vmDriver == "docker-desktop" then [ + "nginx", + "nginx/nodeport", + "nginx/coredns", + "nginx/flux-webhook", + "nginx/web" + ] else [ + "nginx", + "nginx/loadbalancer", + "nginx/coredns", + "nginx/flux-webhook", + "nginx/web" + ], + }, + { + name: "pki-base", + source: "core", + path: "pki/base", + dependsOn: [ + "policy-resources" + ], + force: true, + components: [ + "cert-manager", + "trust-manager" + ], + }, + { + name: "pki-resources", + source: "core", + path: "pki/resources", + dependsOn: [ + "pki-base" + ], + force: true, + components: [ + "private-issuer/ca", + "public-issuer/selfsigned" + ], + }, + { + name: "dns", + source: "core", + path: "dns", + dependsOn: [ + "pki-base" + ], + force: true, + components: if vmDriver == "docker-desktop" then [ + "coredns", + "coredns/etcd", + "external-dns", + "external-dns/localhost", + "external-dns/coredns", + "external-dns/ingress" + ] else [ + "coredns", + "coredns/etcd", + "external-dns", + "external-dns/coredns", + "external-dns/ingress" + ], + }, + { + name: "gitops", + source: "core", + path: "gitops/flux", + dependsOn: [ + "ingress-base" + ], + force: true, + components: [ + "webhook" + ], + }, + { + name: "observability", + source: "core", + path: "observability", + dependsOn: [ + "ingress-base" + ], + components: [ + "grafana", + "grafana/ingress", + "grafana/prometheus", + "grafana/node", + "grafana/kubernetes", + "grafana/flux" + ], + } +] else []; + +// Blueprint metadata +local blueprintMetadata = { + kind: "Blueprint", + apiVersion: "blueprints.windsorcli.dev/v1alpha1", + metadata: { + name: context.name, + description: "This blueprint outlines resources in the " + context.name + " context", + }, +}; + +// Source configuration +local sourceConfig = [ + { + name: "core", + url: "github.com/windsorcli/core", + ref: { + branch: "main", + }, + }, +]; + +// Start of Blueprint +blueprintMetadata + { + repository: repositoryConfig, + sources: sourceConfig, + terraform: terraformConfig, + kustomize: kustomizeConfig, +} diff --git a/pkg/blueprint/templates/metal.jsonnet b/pkg/blueprint/templates/metal.jsonnet new file mode 100644 index 000000000..7d0053c45 --- /dev/null +++ b/pkg/blueprint/templates/metal.jsonnet @@ -0,0 +1,378 @@ +local context = std.extVar("context"); + +// Safely access control plane nodes from the context, defaulting to an empty array if not present +local cpNodes = if std.objectHas(context, "cluster") && std.objectHas(context.cluster, "controlplanes") && std.objectHas(context.cluster.controlplanes, "nodes") + then std.objectValues(context.cluster.controlplanes.nodes) + else []; + +// Select the first node or default to null if no nodes are present +local firstNode = if std.length(cpNodes) > 0 then cpNodes[0] else null; + +// Extract baseUrl from endpoint +local extractBaseUrl(endpoint) = + if endpoint == "" then "" else + local parts = std.split(endpoint, "://"); + if std.length(parts) > 1 then + local hostParts = std.split(parts[1], ":"); + hostParts[0] + else + local hostParts = std.split(endpoint, ":"); + hostParts[0]; + +// Determine the endpoint, using cluster.endpoint if available, otherwise falling back to firstNode +local endpoint = if std.objectHas(context.cluster, "endpoint") then context.cluster.endpoint else if firstNode != null then firstNode.endpoint else ""; +local baseUrl = extractBaseUrl(endpoint); + +// Build certSANs list +local certSANs = ["localhost", baseUrl] + (if std.objectHas(context.cluster, "controlplanes") && std.objectHas(context.cluster.controlplanes, "nodes") && std.length(std.objectValues(context.cluster.controlplanes.nodes)) > 0 then + local firstNode = std.objectValues(context.cluster.controlplanes.nodes)[0]; + local hostname = firstNode.hostname; + local domain = if std.objectHas(context, "dns") && std.objectHas(context.dns, "domain") then context.dns.domain else ""; + [hostname] + (if domain != "" then [hostname + "." + domain] else []) +else []); + +// Build the mirrors dynamically, only if registries are defined +local registryMirrors = if std.objectHas(context, "docker") && std.objectHas(context.docker, "registries") then + std.foldl( + function(acc, key) + local registryInfo = context.docker.registries[key]; + local localOverride = if std.objectHas(registryInfo, "local") + then + local parts = std.split(registryInfo["local"], "//"); + if std.length(parts) > 1 then parts[1] else registryInfo["local"] + else ""; + + if std.objectHas(registryInfo, "hostname") && registryInfo.hostname != "" then + acc + { + [(if localOverride != "" then localOverride else key)]: { + endpoints: ["http://" + registryInfo.hostname + ":5000"], + }, + } + else + acc, + std.objectFields(context.docker.registries), + {} + ) +else {}; + +// Repository configuration +local repositoryConfig = { + url: "", + ref: { + branch: "main", + }, + secretName: "flux-system", +}; + +// Terraform configuration +local terraformConfig = [ + { + path: "cluster/talos", + source: "core", + values: { + cluster_endpoint: if endpoint != "" then "https://" + baseUrl + ":6443" else "", + cluster_name: "talos", + controlplanes: if std.objectHas(context.cluster, "controlplanes") && std.objectHas(context.cluster.controlplanes, "nodes") then + std.map( + function(v) { + endpoint: v.endpoint, + node: v.node, + }, + std.objectValues(context.cluster.controlplanes.nodes) + ) + else [], + workers: if std.objectHas(context.cluster, "workers") && std.objectHas(context.cluster.workers, "nodes") then + std.map( + function(v) { + endpoint: v.endpoint, + node: v.node, + }, + std.objectValues(context.cluster.workers.nodes) + ) + else [], + common_config_patches: std.manifestYamlDoc( + { + cluster: { + apiServer: { + certSANs: certSANs, + }, + extraManifests: [ + "https://raw.githubusercontent.com/alex1989hu/kubelet-serving-cert-approver/v0.8.7/deploy/standalone-install.yaml", + ], + }, + machine: { + certSANs: certSANs, + kubelet: { + extraArgs: { + "rotate-server-certificates": "true", + }, + }, + }, + } + + + (if std.length(std.objectFields(registryMirrors)) == 0 then + {} + else + { + machine+: { + registries: { + mirrors: registryMirrors, + }, + }, + }) + ), + worker_config_patches: if std.objectHas(context.cluster, "workers") && std.objectHas(context.cluster.workers, "volumes") && std.length(context.cluster.workers.volumes) > 0 then + std.manifestYamlDoc( + { + machine: { + kubelet: { + extraMounts: std.map( + function(volume) + local parts = std.split(volume, ":"); + { + destination: parts[1], + type: "bind", + source: parts[1], + options: [ + "rbind", + "rw", + ], + }, + context.cluster.workers.volumes + ), + }, + }, + } + ) + else + "", + controlplane_config_patches: if std.objectHas(context.cluster, "controlplanes") && std.objectHas(context.cluster.controlplanes, "volumes") && std.length(context.cluster.controlplanes.volumes) > 0 then + std.manifestYamlDoc( + { + machine: { + kubelet: { + extraMounts: std.map( + function(volume) + local parts = std.split(volume, ":"); + { + destination: parts[1], + type: "bind", + source: parts[1], + options: [ + "rbind", + "rw", + ], + }, + context.cluster.controlplanes.volumes + ), + }, + }, + } + ) + else + "", + }, + }, + { + path: "gitops/flux", + source: "core", + destroy: false, + } +]; + +// Determine the blueprint, defaulting to an empty string if not defined +local blueprint = if std.objectHas(context, "blueprint") then context.blueprint else ""; + +// Kustomize configuration +local kustomizeConfig = if blueprint == "full" then [ + { + name: "telemetry-base", + source: "core", + path: "telemetry/base", + components: [ + "prometheus", + "prometheus/flux" + ], + }, + { + name: "telemetry-resources", + source: "core", + path: "telemetry/resources", + dependsOn: [ + "telemetry-base" + ], + components: [ + "metrics-server", + "prometheus", + "prometheus/flux" + ], + }, + { + name: "policy-base", + source: "core", + path: "policy/base", + components: [ + "kyverno" + ], + }, + { + name: "policy-resources", + source: "core", + path: "policy/resources", + dependsOn: [ + "policy-base" + ], + }, + { + name: "csi", + source: "core", + path: "csi", + dependsOn: [ + "policy-resources" + ], + force: true, + components: [ + "openebs", + "openebs/dynamic-localpv", + ], + }, + { + name: "lb-base", + source: "core", + path: "lb/base", + dependsOn: [ + "policy-resources" + ], + force: true, + components: [ + "metallb" + ], + }, + { + name: "lb-resources", + source: "core", + path: "lb/resources", + dependsOn: [ + "lb-base" + ], + force: true, + components: [ + "metallb/layer2" + ], + }, + { + name: "ingress-base", + source: "core", + path: "ingress/base", + dependsOn: [ + "pki-resources" + ], + force: true, + components: [ + "nginx", + "nginx/loadbalancer", + "nginx/coredns", + "nginx/flux-webhook", + "nginx/web" + ], + }, + { + name: "pki-base", + source: "core", + path: "pki/base", + dependsOn: [ + "policy-resources" + ], + force: true, + components: [ + "cert-manager", + "trust-manager" + ], + }, + { + name: "pki-resources", + source: "core", + path: "pki/resources", + dependsOn: [ + "pki-base" + ], + force: true, + components: [ + "private-issuer/ca", + "public-issuer/selfsigned" + ], + }, + { + name: "dns", + source: "core", + path: "dns", + dependsOn: [ + "pki-base" + ], + force: true, + components: [ + "coredns", + "coredns/etcd", + "external-dns", + "external-dns/coredns", + "external-dns/ingress" + ], + }, + { + name: "gitops", + source: "core", + path: "gitops/flux", + dependsOn: [ + "ingress-base" + ], + force: true, + components: [ + "webhook" + ], + }, + { + name: "observability", + source: "core", + path: "observability", + dependsOn: [ + "ingress-base" + ], + components: [ + "grafana", + "grafana/ingress", + "grafana/prometheus", + "grafana/node", + "grafana/kubernetes", + "grafana/flux" + ], + } +] else []; + +// Blueprint metadata +local blueprintMetadata = { + kind: "Blueprint", + apiVersion: "blueprints.windsorcli.dev/v1alpha1", + metadata: { + name: context.name, + description: "This blueprint outlines resources in the " + context.name + " context", + }, +}; + +// Source configuration +local sourceConfig = [ + { + name: "core", + url: "github.com/windsorcli/core", + ref: { + branch: "main", + }, + }, +]; + +// Start of Blueprint +blueprintMetadata + { + repository: repositoryConfig, + sources: sourceConfig, + terraform: terraformConfig, + kustomize: kustomizeConfig, +}