Compare commits

...

2 commits

9 changed files with 73 additions and 16 deletions

View file

@ -9,7 +9,7 @@ import (
)
var freshConfigPath, freshOrganization, onboardConfigPath, onboardFrom string
var onboardAppName, onboardAppRepoURL, onboardImageRepository string
var onboardAppName, onboardAppRepoURL, onboardImageRepository, onboardBuildStrategy string
var freshCreateOrganization, freshEnableDelivery, freshYes bool
var freshMode string
@ -54,6 +54,7 @@ func init() {
appOnboardCmd.Flags().StringVar(&onboardAppName, "app-name", "", "Application name override")
appOnboardCmd.Flags().StringVar(&onboardAppRepoURL, "app-repo-url", "", "Application repository URL override")
appOnboardCmd.Flags().StringVar(&onboardImageRepository, "image-repository", "", "OCI image repository override")
appOnboardCmd.Flags().StringVar(&onboardBuildStrategy, "build-strategy", "", "Build strategy override: static or runtime")
_ = appOnboardCmd.MarkFlagRequired("config")
_ = appOnboardCmd.MarkFlagRequired("from")
}
@ -87,6 +88,9 @@ func runAppOnboard(_ *cobra.Command, _ []string) error {
if onboardImageRepository != "" {
cfg.Delivery.ImageRepository = onboardImageRepository
}
if onboardBuildStrategy != "" {
cfg.Delivery.BuildStrategy = onboardBuildStrategy
}
cfg, err = resolveAppOnboarding(cfg)
if err != nil {
return err

View file

@ -65,11 +65,11 @@ func TestBootstrapInitAppliesFluxDefaultsBeforeFreshValidation(t *testing.T) {
func TestAppOnboardValidatesConfigBeforeExternalWork(t *testing.T) {
originalConfig, originalResolve, originalOnboard := loadAppOnboardConfig, resolveAppOnboarding, onboardApp
originalConfigPath, originalFrom := onboardConfigPath, onboardFrom
originalName, originalRepo, originalImage := onboardAppName, onboardAppRepoURL, onboardImageRepository
originalName, originalRepo, originalImage, originalBuildStrategy := onboardAppName, onboardAppRepoURL, onboardImageRepository, onboardBuildStrategy
t.Cleanup(func() {
loadAppOnboardConfig, resolveAppOnboarding, onboardApp = originalConfig, originalResolve, originalOnboard
onboardConfigPath, onboardFrom = originalConfigPath, originalFrom
onboardAppName, onboardAppRepoURL, onboardImageRepository = originalName, originalRepo, originalImage
onboardAppName, onboardAppRepoURL, onboardImageRepository, onboardBuildStrategy = originalName, originalRepo, originalImage, originalBuildStrategy
})
loadAppOnboardConfig = func(string) (config.Config, error) { return config.Config{}, nil }
resolveAppOnboarding = func(config.Config) (config.Config, error) { return config.Config{}, errors.New("incomplete delivery") }
@ -78,7 +78,7 @@ func TestAppOnboardValidatesConfigBeforeExternalWork(t *testing.T) {
return nil
}
onboardConfigPath, onboardFrom = "private.yaml", "app-checkout"
onboardAppName, onboardAppRepoURL, onboardImageRepository = "", "", ""
onboardAppName, onboardAppRepoURL, onboardImageRepository, onboardBuildStrategy = "", "", "", ""
if err := runAppOnboard(nil, nil); err == nil {
t.Fatal("onboarding accepted invalid configuration")
}
@ -87,25 +87,25 @@ func TestAppOnboardValidatesConfigBeforeExternalWork(t *testing.T) {
func TestAppOnboardPassesOnlyValidatedConfigAndCheckout(t *testing.T) {
originalConfig, originalResolve, originalOnboard := loadAppOnboardConfig, resolveAppOnboarding, onboardApp
originalConfigPath, originalFrom := onboardConfigPath, onboardFrom
originalName, originalRepo, originalImage := onboardAppName, onboardAppRepoURL, onboardImageRepository
originalName, originalRepo, originalImage, originalBuildStrategy := onboardAppName, onboardAppRepoURL, onboardImageRepository, onboardBuildStrategy
t.Cleanup(func() {
loadAppOnboardConfig, resolveAppOnboarding, onboardApp = originalConfig, originalResolve, originalOnboard
onboardConfigPath, onboardFrom = originalConfigPath, originalFrom
onboardAppName, onboardAppRepoURL, onboardImageRepository = originalName, originalRepo, originalImage
onboardAppName, onboardAppRepoURL, onboardImageRepository, onboardBuildStrategy = originalName, originalRepo, originalImage, originalBuildStrategy
})
cfg := config.Config{Delivery: config.DeliveryConfig{AppRepoURL: "https://git.example.test/new-org/app.git", AppRepoRef: "main"}}
loadAppOnboardConfig = func(string) (config.Config, error) { return cfg, nil }
resolveAppOnboarding = func(got config.Config) (config.Config, error) { return got, nil }
calls := 0
onboardApp = func(got config.Config, checkout string) error {
if checkout != "app-checkout" || got.Delivery.AppName != "fixture" || got.Delivery.AppRepoURL != "https://git.example.test/new-org/fixture.git" || got.Delivery.ImageRepository != "registry.example.test/new-org/fixture" {
if checkout != "app-checkout" || got.Delivery.AppName != "fixture" || got.Delivery.AppRepoURL != "https://git.example.test/new-org/fixture.git" || got.Delivery.ImageRepository != "registry.example.test/new-org/fixture" || got.Delivery.BuildStrategy != "runtime" {
t.Fatal("onboarding used the wrong checkout or config")
}
calls++
return nil
}
onboardConfigPath, onboardFrom = "private.yaml", "app-checkout"
onboardAppName, onboardAppRepoURL, onboardImageRepository = "fixture", "https://git.example.test/new-org/fixture.git", "registry.example.test/new-org/fixture"
onboardAppName, onboardAppRepoURL, onboardImageRepository, onboardBuildStrategy = "fixture", "https://git.example.test/new-org/fixture.git", "registry.example.test/new-org/fixture", "runtime"
if err := runAppOnboard(nil, nil); err != nil || calls != 1 {
t.Fatalf("runAppOnboard() = %v, calls = %d", err, calls)
}

View file

@ -635,7 +635,7 @@ spec:
operator: notin
values: [closed]
taskRef:
name: maidn-node-static-image
name: {{ if eq .BuildStrategy "static" }}maidn-node-static-image{{ else }}maidn-node-runtime-image{{ end }}
params:
- name: url
value: {{ quote .AppRepoURL }}
@ -643,10 +643,12 @@ spec:
value: $(params.git-revision)
- name: image
value: $(params.image)
{{ if eq .BuildStrategy "static" }}
- name: output-directory
value: {{ quote .BuildOutputDirectory }}
- name: build-configuration
value: {{ quote .BuildConfiguration }}
{{ end }}
- name: update-preview
runAfter: [build-layer]
when:

View file

@ -624,6 +624,7 @@ type appDeliveryTemplateConfig struct {
AppRepoRef string
ProductionBranch string
ImageRepository string
BuildStrategy string
BuildOutputDirectory string
BuildConfiguration string
ForgejoBaseURL string
@ -693,6 +694,9 @@ func renderAppDelivery(cfg config.Config) ([]byte, error) {
if !deliveryAppName.MatchString(cfg.Delivery.AppName) {
return nil, errors.New("delivery appName must be a lowercase DNS label")
}
if cfg.Delivery.BuildStrategy != "static" && cfg.Delivery.BuildStrategy != "runtime" {
return nil, errors.New("delivery buildStrategy must be static or runtime")
}
appRepository, err := deliveryRepository(cfg.Git.BaseURL, cfg.Delivery.AppRepoURL)
if err != nil {
return nil, err
@ -704,7 +708,7 @@ func renderAppDelivery(cfg config.Config) ([]byte, error) {
values := appDeliveryTemplateConfig{
AppName: cfg.Delivery.AppName, AppRepository: appRepository, AppRepoURL: cfg.Delivery.AppRepoURL,
AppRepoRef: cfg.Delivery.AppRepoRef, ProductionBranch: cfg.Delivery.ProductionBranch, ImageRepository: cfg.Delivery.ImageRepository,
BuildOutputDirectory: cfg.Delivery.BuildOutputDirectory, BuildConfiguration: cfg.Delivery.BuildConfiguration,
BuildStrategy: cfg.Delivery.BuildStrategy, BuildOutputDirectory: cfg.Delivery.BuildOutputDirectory, BuildConfiguration: cfg.Delivery.BuildConfiguration,
ForgejoBaseURL: origin, ForgejoOwner: cfg.Git.Owner, ManifestsURL: forgejo.CloneURL(origin, cfg.Git.Owner, cfg.Flux.ManifestsRepo), ManifestsRepo: cfg.Flux.ManifestsRepo, ManifestsBranch: cfg.Flux.Branch,
}
for name, value := range map[string]string{"appRepository": values.AppRepository, "appRepoUrl": values.AppRepoURL, "appRepoRef": values.AppRepoRef, "productionBranch": values.ProductionBranch, "imageRepository": values.ImageRepository, "buildOutputDirectory": values.BuildOutputDirectory, "buildConfiguration": values.BuildConfiguration, "forgejoBaseUrl": values.ForgejoBaseURL, "forgejoOwner": values.ForgejoOwner, "manifestsUrl": values.ManifestsURL, "manifestsRepo": values.ManifestsRepo, "manifestsBranch": values.ManifestsBranch} {

View file

@ -112,13 +112,13 @@ func TestGeneratedDeliveryUsesCombinedStaticBuildArtifactContract(t *testing.T)
cfg := config.Config{
Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "platform"},
Flux: config.FluxConfig{Branch: "main", ManifestsRepo: "manifests"},
Delivery: config.DeliveryConfig{AppName: "web-ui", AppRepoURL: "https://git.example.test/apps/web-ui.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/apps/web-ui", BuildOutputDirectory: "dist/web-ui", BuildConfiguration: "production"},
Delivery: config.DeliveryConfig{AppName: "web-ui", AppRepoURL: "https://git.example.test/apps/web-ui.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/apps/web-ui", BuildStrategy: "static", BuildOutputDirectory: "dist/web-ui", BuildConfiguration: "production"},
}
content, err := renderAppDelivery(cfg)
if err != nil {
t.Fatal(err)
}
for _, expected := range []string{"name: build-layer", "maidn-node-static-image", "name: url\n value: \"https://git.example.test/apps/web-ui.git\"", "name: revision\n value: $(params.git-revision)", "name: image\n value: $(params.image)", "runAfter: [build-layer]", "maidn-preview-orphan-reconciler", "valid_pr_number()", "valid_commit()", "values: [promotion]", "values: [\"production\"]", "cmp -s \"$expected_marker\" \"$marker\"", "values: [closed]"} {
for _, expected := range []string{"name: build-layer", "maidn-node-static-image", "name: url\n value: \"https://git.example.test/apps/web-ui.git\"", "name: revision\n value: $(params.git-revision)", "name: image\n value: $(params.image)", "name: output-directory\n value: \"dist/web-ui\"", "name: build-configuration\n value: \"production\"", "runAfter: [build-layer]", "maidn-preview-orphan-reconciler", "valid_pr_number()", "valid_commit()", "values: [promotion]", "values: [\"production\"]", "cmp -s \"$expected_marker\" \"$marker\"", "values: [closed]"} {
if !strings.Contains(string(content), expected) {
t.Fatalf("generated delivery does not contain %q", expected)
}
@ -130,11 +130,33 @@ func TestGeneratedDeliveryUsesCombinedStaticBuildArtifactContract(t *testing.T)
}
}
func TestGeneratedDeliveryUsesRuntimeImageWithoutStaticParameters(t *testing.T) {
cfg := config.Config{
Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "platform"},
Flux: config.FluxConfig{Branch: "main", ManifestsRepo: "manifests"},
Delivery: config.DeliveryConfig{AppName: "web-ui", AppRepoURL: "https://git.example.test/apps/web-ui.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/apps/web-ui", BuildStrategy: "runtime", BuildOutputDirectory: "dist/web-ui", BuildConfiguration: "production"},
}
content, err := renderAppDelivery(cfg)
if err != nil {
t.Fatal(err)
}
for _, expected := range []string{"maidn-node-runtime-image", "name: url\n value: \"https://git.example.test/apps/web-ui.git\"", "name: revision\n value: $(params.git-revision)", "name: image\n value: $(params.image)"} {
if !strings.Contains(string(content), expected) {
t.Fatalf("generated runtime delivery does not contain %q", expected)
}
}
for _, unexpected := range []string{"maidn-node-static-image", "name: output-directory", "name: build-configuration"} {
if strings.Contains(string(content), unexpected) {
t.Fatalf("generated runtime delivery contains static-only %q", unexpected)
}
}
}
func TestGeneratedDeliveryInitializesStagingAndPromotesByPullRequest(t *testing.T) {
cfg := config.Config{
Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "platform"},
Flux: config.FluxConfig{Branch: "main", ManifestsRepo: "manifests"},
Delivery: config.DeliveryConfig{AppName: "web-ui", AppRepoURL: "https://git.example.test/apps/web-ui.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/apps/web-ui", BuildOutputDirectory: "dist", BuildConfiguration: "production"},
Delivery: config.DeliveryConfig{AppName: "web-ui", AppRepoURL: "https://git.example.test/apps/web-ui.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/apps/web-ui", BuildStrategy: "static", BuildOutputDirectory: "dist", BuildConfiguration: "production"},
}
content, err := renderAppDelivery(cfg)
if err != nil {

View file

@ -32,7 +32,7 @@ func onboardingConfig() config.Config {
Flux: config.FluxConfig{Branch: "main", RepoName: "cluster", ManifestsRepo: "manifests", ClusterDomain: "example.test"},
Delivery: config.DeliveryConfig{
AppName: "web-ui", AppRepoURL: "https://git.example.test/test-org-2/web-ui.git", AppRepoRef: "main", ProductionBranch: "production",
ImageRepository: "registry.example.test/test-org-2/web-ui", BuildOutputDirectory: "dist", BuildConfiguration: "production",
ImageRepository: "registry.example.test/test-org-2/web-ui", BuildStrategy: "static", BuildOutputDirectory: "dist", BuildConfiguration: "production",
WebhookHostname: "tekton.example.test", WebhookPath: "/",
},
}

View file

@ -215,6 +215,9 @@ func validateRepositoryURL(value string) error {
}
func applyDefaults(cfg *Config) {
if cfg.Delivery.BuildStrategy == "" {
cfg.Delivery.BuildStrategy = "static"
}
if cfg.ClusterID == "" {
cfg.ClusterID = cfg.Talos.Cluster.Name
}
@ -317,6 +320,9 @@ func applyDefaults(cfg *Config) {
}
func applyDeliveryDefaults(cfg *Config) {
if cfg.Delivery.BuildStrategy == "" {
cfg.Delivery.BuildStrategy = "static"
}
if cfg.Delivery.AppRepoRef == "" {
cfg.Delivery.AppRepoRef = cfg.Flux.Branch
}
@ -542,6 +548,9 @@ func ValidateDelivery(cfg Config) error {
if !regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,45}[a-z0-9])?$`).MatchString(cfg.Delivery.AppName) {
return errors.New("delivery appName must be a lowercase DNS label of at most 47 characters")
}
if cfg.Delivery.BuildStrategy != "static" && cfg.Delivery.BuildStrategy != "runtime" {
return errors.New("delivery buildStrategy must be static or runtime")
}
if cfg.Delivery.ProductionBranch == cfg.Delivery.AppRepoRef || !validDeliveryBranch(cfg.Delivery.ProductionBranch) {
return errors.New("delivery productionBranch must be a valid branch distinct from appRepoRef")
}

View file

@ -17,7 +17,7 @@ func validConfig(t *testing.T) Config {
Templates: TemplateConfig{TalosRepoURL: "https://git.example.test/talos.git", TalosRepoRef: "main", CICDRepoURL: "https://git.example.test/template.git", CICDRepoRef: "main", ManifestsRepoURL: "https://git.example.test/manifests.git", ManifestsRepoRef: "main"},
Cilium: CiliumConfig{TrafficInterface: "eth1", LoadBalancerStart: "192.168.45.19", LoadBalancerEnd: "192.168.45.30"},
DemocraticCSI: DemocraticCSIConfig{TrueNASAPIKey: "api-key", TrueNASHost: "truenas.example.test", TargetPortal: "truenas.example.test:3260", ShareHost: "truenas.example.test", DatasetParentNFS: "pool/kubernetes/nfs/v", DatasetSnapshotsNFS: "pool/kubernetes/nfs/s", AllowedNetworks: "192.168.45.0/24", NameSuffix: "-test", PortalGroup: "1", InitiatorGroup: "1"},
Delivery: DeliveryConfig{AppName: "web-ui", AppRepoURL: "https://git.example.test/test-org/web-ui.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/test-org/web-ui", BuildOutputDirectory: "dist", BuildConfiguration: "production", WebhookHostname: "tekton.example.test", WebhookPath: "/"},
Delivery: DeliveryConfig{AppName: "web-ui", AppRepoURL: "https://git.example.test/test-org/web-ui.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/test-org/web-ui", BuildStrategy: "static", BuildOutputDirectory: "dist", BuildConfiguration: "production", WebhookHostname: "tekton.example.test", WebhookPath: "/"},
Talos: TalosConfig{
RepoDirName: "talos", TerraformDir: "terraform", GeneratedDir: "generated", ConfigFileName: "terraform.tfvars",
Proxmox: TalosProxmoxConfig{APIURL: "https://proxmox.example.test:8006", APITokenID: "id", APITokenSecret: "secret"},
@ -64,6 +64,7 @@ func TestResolveDeliveryAppliesDefaultsOnlyForExplicitAppOperations(t *testing.T
cfg.Delivery.AppRepoRef = ""
cfg.Delivery.BuildOutputDirectory = ""
cfg.Delivery.BuildConfiguration = ""
cfg.Delivery.BuildStrategy = ""
cfg.Delivery.WebhookHostname = ""
cfg.Delivery.WebhookPath = ""
@ -78,11 +79,25 @@ func TestResolveDeliveryAppliesDefaultsOnlyForExplicitAppOperations(t *testing.T
if err != nil {
t.Fatal(err)
}
if !delivery.Delivery.Configured() || delivery.Delivery.AppRepoRef != "main" || delivery.Delivery.BuildOutputDirectory != "dist" || delivery.Delivery.BuildConfiguration != "production" || delivery.Delivery.WebhookURL() != "https://tekton.example.test/" {
if !delivery.Delivery.Configured() || delivery.Delivery.BuildStrategy != "static" || delivery.Delivery.AppRepoRef != "main" || delivery.Delivery.BuildOutputDirectory != "dist" || delivery.Delivery.BuildConfiguration != "production" || delivery.Delivery.WebhookURL() != "https://tekton.example.test/" {
t.Fatalf("ResolveDelivery() did not apply the complete delivery contract: %#v", delivery.Delivery)
}
}
func TestValidateDeliveryBuildStrategy(t *testing.T) {
cfg := validConfig(t)
for _, strategy := range []string{"static", "runtime"} {
cfg.Delivery.BuildStrategy = strategy
if err := ValidateDelivery(cfg); err != nil {
t.Fatalf("ValidateDelivery() rejected %q: %v", strategy, err)
}
}
cfg.Delivery.BuildStrategy = "container"
if err := ValidateDelivery(cfg); err == nil || !strings.Contains(err.Error(), "buildStrategy") {
t.Fatalf("ValidateDelivery() accepted invalid build strategy: %v", err)
}
}
func TestValidateDeliveryRequiresCompleteConfig(t *testing.T) {
cfg := validConfig(t)
cfg.Delivery.ImageRepository = ""

View file

@ -45,6 +45,7 @@ type DeliveryConfig struct {
AppRepoRef string `yaml:"appRepoRef"`
ProductionBranch string `yaml:"productionBranch"`
ImageRepository string `yaml:"imageRepository"`
BuildStrategy string `yaml:"buildStrategy"`
BuildOutputDirectory string `yaml:"buildOutputDirectory"`
BuildConfiguration string `yaml:"buildConfiguration"`
TektonDashboardURL string `yaml:"tektonDashboardUrl,omitempty"`