Compare commits
No commits in common. "main" and "feat/central-app-delivery-resources" have entirely different histories.
main
...
feat/centr
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
This is the approved target architecture. Central onboarding is available for
|
This is the approved target architecture. The current source-owned onboarding
|
||||||
new applications; existing source-owned registrations remain migration work.
|
implementation is being migrated and must not be used for new applications.
|
||||||
|
|
||||||
## Trust Boundary
|
## Trust Boundary
|
||||||
|
|
||||||
|
|
@ -24,10 +24,6 @@ operators and approved automation may push or merge into it. Flux must track
|
||||||
only that branch for chart content. Flux must never track an application `main`
|
only that branch for chart content. Flux must never track an application `main`
|
||||||
branch or `maidn/delivery-*` branch.
|
branch or `maidn/delivery-*` branch.
|
||||||
|
|
||||||
Onboarding requires an existing `maidn/platform-<app>` branch and verifies its
|
|
||||||
no-direct-push protection before it opens the central registration PR. It never
|
|
||||||
seeds a platform branch from developer-controlled `main`.
|
|
||||||
|
|
||||||
## Resource Flow
|
## Resource Flow
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,8 @@ go run . bootstrap init --config <private-bootstrap-config> --organization <owne
|
||||||
The source-owned onboarding implementation is retired. Central onboarding
|
The source-owned onboarding implementation is retired. Central onboarding
|
||||||
creates a reviewed cluster registration only; it never writes `.tekton` or
|
creates a reviewed cluster registration only; it never writes `.tekton` or
|
||||||
`.maidn` resources to an application repository. Before merging that
|
`.maidn` resources to an application repository. Before merging that
|
||||||
registration, a platform operator must create the corresponding
|
registration, a platform operator must create and protect the corresponding
|
||||||
`maidn/platform-<app>` package through a reviewed platform PR; onboarding
|
`maidn/platform-<app>` package through a reviewed platform PR. See [Delivery
|
||||||
verifies its existence and enforces its protection. See [Delivery
|
|
||||||
Ownership](architecture/delivery-ownership.md) for the approved architecture
|
Ownership](architecture/delivery-ownership.md) for the approved architecture
|
||||||
and migration rules.
|
and migration rules.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ spec:
|
||||||
- name: environment
|
- name: environment
|
||||||
- name: pr-number
|
- name: pr-number
|
||||||
default: ""
|
default: ""
|
||||||
|
- name: platform-url
|
||||||
|
default: ""
|
||||||
|
- name: platform-revision
|
||||||
|
default: ""
|
||||||
- name: forgejo-base-url
|
- name: forgejo-base-url
|
||||||
default: ""
|
default: ""
|
||||||
- name: forgejo-owner
|
- name: forgejo-owner
|
||||||
|
|
@ -53,6 +57,10 @@ spec:
|
||||||
value: $(params.environment)
|
value: $(params.environment)
|
||||||
- name: PR_NUMBER
|
- name: PR_NUMBER
|
||||||
value: $(params.pr-number)
|
value: $(params.pr-number)
|
||||||
|
- name: PLATFORM_URL
|
||||||
|
value: $(params.platform-url)
|
||||||
|
- name: PLATFORM_REVISION
|
||||||
|
value: $(params.platform-revision)
|
||||||
- name: FORGEJO_BASE_URL
|
- name: FORGEJO_BASE_URL
|
||||||
value: $(params.forgejo-base-url)
|
value: $(params.forgejo-base-url)
|
||||||
- name: FORGEJO_OWNER
|
- name: FORGEJO_OWNER
|
||||||
|
|
@ -94,6 +102,8 @@ spec:
|
||||||
cd /tmp/manifests
|
cd /tmp/manifests
|
||||||
if [ "$ENVIRONMENT" = preview ]; then
|
if [ "$ENVIRONMENT" = preview ]; then
|
||||||
valid_pr_number "$PR_NUMBER"
|
valid_pr_number "$PR_NUMBER"
|
||||||
|
valid_url "$PLATFORM_URL"
|
||||||
|
valid_revision "$PLATFORM_REVISION"
|
||||||
app_dir="apps/previews/$APP_NAME-pr-$PR_NUMBER"
|
app_dir="apps/previews/$APP_NAME-pr-$PR_NUMBER"
|
||||||
marker="$app_dir/ownership.yaml"
|
marker="$app_dir/ownership.yaml"
|
||||||
if [ -e "$app_dir" ]; then
|
if [ -e "$app_dir" ]; then
|
||||||
|
|
@ -119,7 +129,12 @@ spec:
|
||||||
EOF
|
EOF
|
||||||
cmp -s "$expected_marker" "$marker" || fail
|
cmp -s "$expected_marker" "$marker" || fail
|
||||||
fi
|
fi
|
||||||
|
git clone "$PLATFORM_URL" /tmp/platform
|
||||||
|
git -C /tmp/platform checkout "$PLATFORM_REVISION"
|
||||||
|
[ -f /tmp/platform/preview/values.yaml ] || fail
|
||||||
mkdir -p "$app_dir"
|
mkdir -p "$app_dir"
|
||||||
|
cp /tmp/platform/preview/values.yaml "$app_dir/values.yaml"
|
||||||
|
sed -i "s/PLACEHOLDER_PR/$PR_NUMBER/g" "$app_dir/values.yaml"
|
||||||
cat > "$marker" <<EOF
|
cat > "$marker" <<EOF
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: ConfigMap
|
kind: ConfigMap
|
||||||
|
|
@ -154,20 +169,14 @@ spec:
|
||||||
chart:
|
chart:
|
||||||
spec:
|
spec:
|
||||||
chart: ./charts/$APP_NAME
|
chart: ./charts/$APP_NAME
|
||||||
reconcileStrategy: Revision
|
|
||||||
sourceRef:
|
sourceRef:
|
||||||
kind: GitRepository
|
kind: GitRepository
|
||||||
name: $APP_NAME
|
name: $APP_NAME
|
||||||
namespace: flux-system
|
namespace: flux-system
|
||||||
valuesFiles:
|
|
||||||
- ./charts/$APP_NAME/values.yaml
|
|
||||||
- ./preview/values.yaml
|
|
||||||
values:
|
values:
|
||||||
image:
|
image:
|
||||||
repository: $IMAGE
|
repository: $IMAGE
|
||||||
tag: $TAG
|
tag: $TAG
|
||||||
gateway:
|
|
||||||
hostname: $APP_NAME-pr-$PR_NUMBER.{{ .ClusterDomain }}
|
|
||||||
EOF
|
EOF
|
||||||
cat > "$app_dir/kustomization.yaml" <<EOF
|
cat > "$app_dir/kustomization.yaml" <<EOF
|
||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
|
@ -203,14 +212,10 @@ spec:
|
||||||
chart:
|
chart:
|
||||||
spec:
|
spec:
|
||||||
chart: ./charts/$APP_NAME
|
chart: ./charts/$APP_NAME
|
||||||
reconcileStrategy: Revision
|
|
||||||
sourceRef:
|
sourceRef:
|
||||||
kind: GitRepository
|
kind: GitRepository
|
||||||
name: $APP_NAME
|
name: $APP_NAME
|
||||||
namespace: flux-system
|
namespace: flux-system
|
||||||
valuesFiles:
|
|
||||||
- ./charts/$APP_NAME/values.yaml
|
|
||||||
- ./staging/values.yaml
|
|
||||||
values:
|
values:
|
||||||
image:
|
image:
|
||||||
repository: $IMAGE
|
repository: $IMAGE
|
||||||
|
|
@ -254,14 +259,10 @@ spec:
|
||||||
chart:
|
chart:
|
||||||
spec:
|
spec:
|
||||||
chart: ./charts/$APP_NAME
|
chart: ./charts/$APP_NAME
|
||||||
reconcileStrategy: Revision
|
|
||||||
sourceRef:
|
sourceRef:
|
||||||
kind: GitRepository
|
kind: GitRepository
|
||||||
name: $APP_NAME
|
name: $APP_NAME
|
||||||
namespace: flux-system
|
namespace: flux-system
|
||||||
valuesFiles:
|
|
||||||
- ./charts/$APP_NAME/values.yaml
|
|
||||||
- ./production/values.yaml
|
|
||||||
values:
|
values:
|
||||||
image:
|
image:
|
||||||
repository: $IMAGE
|
repository: $IMAGE
|
||||||
|
|
@ -627,6 +628,10 @@ spec:
|
||||||
default: {{ quote .ForgejoOwner }}
|
default: {{ quote .ForgejoOwner }}
|
||||||
- name: manifests-repo
|
- name: manifests-repo
|
||||||
default: {{ quote .ManifestsRepo }}
|
default: {{ quote .ManifestsRepo }}
|
||||||
|
- name: platform-url
|
||||||
|
default: {{ quote .AppRepoURL }}
|
||||||
|
- name: platform-revision
|
||||||
|
default: {{ quote .PlatformBranch }}
|
||||||
tasks:
|
tasks:
|
||||||
- name: build-layer
|
- name: build-layer
|
||||||
when:
|
when:
|
||||||
|
|
@ -676,6 +681,10 @@ spec:
|
||||||
value: preview
|
value: preview
|
||||||
- name: pr-number
|
- name: pr-number
|
||||||
value: $(params.pr-number)
|
value: $(params.pr-number)
|
||||||
|
- name: platform-url
|
||||||
|
value: $(params.platform-url)
|
||||||
|
- name: platform-revision
|
||||||
|
value: $(params.platform-revision)
|
||||||
- name: update-staging
|
- name: update-staging
|
||||||
runAfter: [build-layer]
|
runAfter: [build-layer]
|
||||||
when:
|
when:
|
||||||
|
|
|
||||||
|
|
@ -622,6 +622,7 @@ type appDeliveryTemplateConfig struct {
|
||||||
AppRepository string
|
AppRepository string
|
||||||
AppRepoURL string
|
AppRepoURL string
|
||||||
AppRepoRef string
|
AppRepoRef string
|
||||||
|
PlatformBranch string
|
||||||
ProductionBranch string
|
ProductionBranch string
|
||||||
ImageRepository string
|
ImageRepository string
|
||||||
BuildStrategy string
|
BuildStrategy string
|
||||||
|
|
@ -632,7 +633,6 @@ type appDeliveryTemplateConfig struct {
|
||||||
ManifestsURL string
|
ManifestsURL string
|
||||||
ManifestsRepo string
|
ManifestsRepo string
|
||||||
ManifestsBranch string
|
ManifestsBranch string
|
||||||
ClusterDomain string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var deliveryAppName = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,45}[a-z0-9])?$`)
|
var deliveryAppName = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,45}[a-z0-9])?$`)
|
||||||
|
|
@ -654,11 +654,11 @@ func renderAppDelivery(cfg config.Config) ([]byte, error) {
|
||||||
}
|
}
|
||||||
values := appDeliveryTemplateConfig{
|
values := appDeliveryTemplateConfig{
|
||||||
AppName: cfg.Delivery.AppName, AppRepository: appRepository, AppRepoURL: cfg.Delivery.AppRepoURL,
|
AppName: cfg.Delivery.AppName, AppRepository: appRepository, AppRepoURL: cfg.Delivery.AppRepoURL,
|
||||||
AppRepoRef: cfg.Delivery.AppRepoRef, ProductionBranch: cfg.Delivery.ProductionBranch, ImageRepository: cfg.Delivery.ImageRepository,
|
AppRepoRef: cfg.Delivery.AppRepoRef, PlatformBranch: "maidn/platform-" + cfg.Delivery.AppName, ProductionBranch: cfg.Delivery.ProductionBranch, ImageRepository: cfg.Delivery.ImageRepository,
|
||||||
BuildStrategy: cfg.Delivery.BuildStrategy, 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, ClusterDomain: cfg.Flux.ClusterDomain,
|
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, "clusterDomain": values.ClusterDomain} {
|
for name, value := range map[string]string{"appRepository": values.AppRepository, "appRepoUrl": values.AppRepoURL, "appRepoRef": values.AppRepoRef, "platformBranch": values.PlatformBranch, "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} {
|
||||||
if value == "" || strings.ContainsAny(value, "\r\n") || config.RedactURL(value) != value {
|
if value == "" || strings.ContainsAny(value, "\r\n") || config.RedactURL(value) != value {
|
||||||
return nil, fmt.Errorf("delivery %s cannot be empty or contain credentials", name)
|
return nil, fmt.Errorf("delivery %s cannot be empty or contain credentials", name)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ func TestRemoveDuplicateAppDeliverySource(t *testing.T) {
|
||||||
func TestGeneratedDeliveryUsesCombinedStaticBuildArtifactContract(t *testing.T) {
|
func TestGeneratedDeliveryUsesCombinedStaticBuildArtifactContract(t *testing.T) {
|
||||||
cfg := config.Config{
|
cfg := config.Config{
|
||||||
Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "platform"},
|
Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "platform"},
|
||||||
Flux: config.FluxConfig{Branch: "main", ClusterDomain: "example.test", ManifestsRepo: "manifests"},
|
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: "static", 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)
|
content, err := renderAppDelivery(cfg)
|
||||||
|
|
@ -123,12 +123,12 @@ func TestGeneratedDeliveryUsesCombinedStaticBuildArtifactContract(t *testing.T)
|
||||||
t.Fatalf("generated delivery does not contain %q", expected)
|
t.Fatalf("generated delivery does not contain %q", expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, expected := range []string{"reconcileStrategy: Revision", "valuesFiles:\n - ./charts/$APP_NAME/values.yaml\n - ./preview/values.yaml", "valuesFiles:\n - ./charts/$APP_NAME/values.yaml\n - ./staging/values.yaml", "valuesFiles:\n - ./charts/$APP_NAME/values.yaml\n - ./production/values.yaml", "hostname: $APP_NAME-pr-$PR_NUMBER.example.test"} {
|
for _, expected := range []string{"name: platform-url\n default: \"https://git.example.test/apps/web-ui.git\"", "name: platform-revision\n default: \"maidn/platform-web-ui\"", "git clone \"$PLATFORM_URL\" /tmp/platform", "/tmp/platform/preview/values.yaml"} {
|
||||||
if !strings.Contains(string(content), expected) {
|
if !strings.Contains(string(content), expected) {
|
||||||
t.Fatalf("generated delivery does not contain protected platform values %q", expected)
|
t.Fatalf("generated delivery does not contain protected platform input %q", expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, unexpected := range []string{"maidn-git-clone", "maidn-node-static-build", "maidn-node-static-push", "runAfter: [clone]", "name: source", "workspace: source", "workspace: artifact", "easycsr", "test-org", "git rm -r", "$APP_URL", "$APP_REVISION", "platform-url", "platform-revision", "/tmp/platform", "/tmp/app/preview/values.yaml"} {
|
for _, unexpected := range []string{"maidn-git-clone", "maidn-node-static-build", "maidn-node-static-push", "runAfter: [clone]", "name: source", "workspace: source", "workspace: artifact", "easycsr", "test-org", "git rm -r", "$APP_URL", "$APP_REVISION", "/tmp/app/preview/values.yaml"} {
|
||||||
if strings.Contains(string(content), unexpected) {
|
if strings.Contains(string(content), unexpected) {
|
||||||
t.Fatalf("generated delivery contains unexpected %q", unexpected)
|
t.Fatalf("generated delivery contains unexpected %q", unexpected)
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +138,7 @@ func TestGeneratedDeliveryUsesCombinedStaticBuildArtifactContract(t *testing.T)
|
||||||
func TestGeneratedDeliveryUsesRuntimeImageWithoutStaticParameters(t *testing.T) {
|
func TestGeneratedDeliveryUsesRuntimeImageWithoutStaticParameters(t *testing.T) {
|
||||||
cfg := config.Config{
|
cfg := config.Config{
|
||||||
Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "platform"},
|
Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "platform"},
|
||||||
Flux: config.FluxConfig{Branch: "main", ClusterDomain: "example.test", ManifestsRepo: "manifests"},
|
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"},
|
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)
|
content, err := renderAppDelivery(cfg)
|
||||||
|
|
@ -160,7 +160,7 @@ func TestGeneratedDeliveryUsesRuntimeImageWithoutStaticParameters(t *testing.T)
|
||||||
func TestGeneratedDeliveryInitializesStagingAndPromotesByPullRequest(t *testing.T) {
|
func TestGeneratedDeliveryInitializesStagingAndPromotesByPullRequest(t *testing.T) {
|
||||||
cfg := config.Config{
|
cfg := config.Config{
|
||||||
Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "platform"},
|
Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "platform"},
|
||||||
Flux: config.FluxConfig{Branch: "main", ClusterDomain: "example.test", ManifestsRepo: "manifests"},
|
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: "static", 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)
|
content, err := renderAppDelivery(cfg)
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,6 @@ func OnboardApp(cfg config.Config, sourceDir string) error {
|
||||||
if sourceBranch != resolved.Delivery.AppRepoRef {
|
if sourceBranch != resolved.Delivery.AppRepoRef {
|
||||||
return errors.New("--from branch must match delivery appRepoRef")
|
return errors.New("--from branch must match delivery appRepoRef")
|
||||||
}
|
}
|
||||||
sourceRevision, err := forgejo.BranchRevision(sourceDir, sourceBranch)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
owner, repository, err := forgejo.RepositoryFromURL(resolved.Delivery.AppRepoURL)
|
owner, repository, err := forgejo.RepositoryFromURL(resolved.Delivery.AppRepoURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -55,16 +51,13 @@ func OnboardApp(cfg config.Config, sourceDir string) error {
|
||||||
if _, err := sourceManager.EnsureRepository(repository, "Application build input for Maidn CI/CD"); err != nil {
|
if _, err := sourceManager.EnsureRepository(repository, "Application build input for Maidn CI/CD"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := publishInitialAppBranches(sourceManager, sourceDir, resolved.Delivery.AppRepoURL, sourceRevision, resolved.Delivery.AppRepoRef, resolved.Delivery.ProductionBranch); err != nil {
|
if err := publishInitialAppBranches(sourceManager, sourceDir, resolved.Delivery.AppRepoURL, sourceBranch, resolved.Delivery.AppRepoRef, resolved.Delivery.ProductionBranch); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := sourceManager.EnsureProtectedBranch(repository, resolved.Delivery.ProductionBranch); err != nil {
|
if err := sourceManager.EnsureProtectedBranch(repository, resolved.Delivery.ProductionBranch); err != nil {
|
||||||
return fmt.Errorf("protect Forgejo production branch: %w", err)
|
return fmt.Errorf("protect Forgejo production branch: %w", err)
|
||||||
}
|
}
|
||||||
if err := ensurePlatformBranch(sourceManager, repository, resolved.Delivery.AppRepoURL, resolved.Delivery.AppName); err != nil {
|
registrationBranch := "maidn/register-" + resolved.Delivery.AppName
|
||||||
return err
|
|
||||||
}
|
|
||||||
registrationBranch := registrationBranch(resolved.Delivery.AppName, sourceRevision)
|
|
||||||
clusterManager := newOnboardingRepoManager(resolved.Git.BaseURL, resolved.Git.Token, resolved.Git.Owner, resolved.Git.Username, "", "", resolved.Flux.Branch, "")
|
clusterManager := newOnboardingRepoManager(resolved.Git.BaseURL, resolved.Git.Token, resolved.Git.Owner, resolved.Git.Username, "", "", resolved.Flux.Branch, "")
|
||||||
if _, err := clusterManager.PublishRepositoryPullRequest(resolved.Flux.RepoName, "feat: register "+resolved.Delivery.AppName+" delivery", registrationBranch, resolved.Flux.Branch, func(dir string) error {
|
if _, err := clusterManager.PublishRepositoryPullRequest(resolved.Flux.RepoName, "feat: register "+resolved.Delivery.AppName+" delivery", registrationBranch, resolved.Flux.Branch, func(dir string) error {
|
||||||
return registerAppInCluster(dir, resolved)
|
return registerAppInCluster(dir, resolved)
|
||||||
|
|
@ -90,31 +83,12 @@ func OnboardApp(cfg config.Config, sourceDir string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func registrationBranch(appName, revision string) string {
|
|
||||||
return "maidn/register-" + appName + "-" + revision[:12]
|
|
||||||
}
|
|
||||||
|
|
||||||
func platformBranch(appName string) string {
|
|
||||||
return "maidn/platform-" + appName
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensurePlatformBranch(manager onboardingRepoManager, repository, repositoryURL, appName string) error {
|
|
||||||
branch := platformBranch(appName)
|
|
||||||
revision, err := manager.RemoteBranchRevision(repositoryURL, branch)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("read Forgejo platform branch: %w", err)
|
|
||||||
}
|
|
||||||
if revision == "" {
|
|
||||||
return fmt.Errorf("approved Forgejo platform branch %q must exist before central registration", branch)
|
|
||||||
}
|
|
||||||
if err := manager.EnsureProtectedBranch(repository, branch); err != nil {
|
|
||||||
return fmt.Errorf("protect Forgejo platform branch: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// publishInitialAppBranches establishes the immutable source baseline before central registration.
|
// publishInitialAppBranches establishes the immutable source baseline before central registration.
|
||||||
func publishInitialAppBranches(manager onboardingRepoManager, sourceDir, targetURL, sourceRevision, targetBranch, productionBranch string) error {
|
func publishInitialAppBranches(manager onboardingRepoManager, sourceDir, targetURL, sourceBranch, targetBranch, productionBranch string) error {
|
||||||
|
sourceRevision, err := forgejo.BranchRevision(sourceDir, sourceBranch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
mainRevision, err := manager.RemoteBranchRevision(targetURL, targetBranch)
|
mainRevision, err := manager.RemoteBranchRevision(targetURL, targetBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read target base branch: %w", err)
|
return fmt.Errorf("read target base branch: %w", err)
|
||||||
|
|
@ -235,8 +209,8 @@ spec:
|
||||||
secretRef:
|
secretRef:
|
||||||
name: forgejo-flux-credentials
|
name: forgejo-flux-credentials
|
||||||
ref:
|
ref:
|
||||||
branch: %s
|
branch: maidn/platform-%s
|
||||||
`, cfg.Delivery.AppName, cfg.Delivery.AppRepoURL, platformBranch(cfg.Delivery.AppName))
|
`, cfg.Delivery.AppName, cfg.Delivery.AppRepoURL, cfg.Delivery.AppName)
|
||||||
if len(secretAccess) != 0 {
|
if len(secretAccess) != 0 {
|
||||||
content += "---\n" + string(secretAccess)
|
content += "---\n" + string(secretAccess)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ func TestPublishInitialAppBranchesCreatesAndPreservesProduction(t *testing.T) {
|
||||||
onboardingGit(t, "", "init", "--bare", target)
|
onboardingGit(t, "", "init", "--bare", target)
|
||||||
|
|
||||||
manager := forgejo.NewRepoManager("https://git.example.test", "", "owner", "", "", "", "main", "")
|
manager := forgejo.NewRepoManager("https://git.example.test", "", "owner", "", "", "", "main", "")
|
||||||
if err := publishInitialAppBranches(manager, source, target, sourceRevision, "main", "production"); err != nil {
|
if err := publishInitialAppBranches(manager, source, target, "source", "main", "production"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
for _, branch := range []string{"main", "production"} {
|
for _, branch := range []string{"main", "production"} {
|
||||||
|
|
@ -139,7 +139,7 @@ func TestPublishInitialAppBranchesCreatesAndPreservesProduction(t *testing.T) {
|
||||||
existingProduction := onboardingGit(t, production, "rev-parse", "production")
|
existingProduction := onboardingGit(t, production, "rev-parse", "production")
|
||||||
onboardingGit(t, production, "push", preservedTarget, "production:production")
|
onboardingGit(t, production, "push", preservedTarget, "production:production")
|
||||||
|
|
||||||
if err := publishInitialAppBranches(manager, source, preservedTarget, sourceRevision, "main", "production"); err != nil {
|
if err := publishInitialAppBranches(manager, source, preservedTarget, "source", "main", "production"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if got := onboardingGit(t, "", "--git-dir", preservedTarget, "rev-parse", "refs/heads/production"); got != existingProduction {
|
if got := onboardingGit(t, "", "--git-dir", preservedTarget, "rev-parse", "refs/heads/production"); got != existingProduction {
|
||||||
|
|
@ -149,7 +149,7 @@ func TestPublishInitialAppBranchesCreatesAndPreservesProduction(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
onboardingGit(t, source, "commit", "-am", "updated source")
|
onboardingGit(t, source, "commit", "-am", "updated source")
|
||||||
if err := publishInitialAppBranches(manager, source, preservedTarget, sourceRevision, "main", "production"); err != nil {
|
if err := publishInitialAppBranches(manager, source, preservedTarget, "source", "main", "production"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if got := onboardingGit(t, "", "--git-dir", preservedTarget, "rev-parse", "refs/heads/main"); got != sourceRevision {
|
if got := onboardingGit(t, "", "--git-dir", preservedTarget, "rev-parse", "refs/heads/main"); got != sourceRevision {
|
||||||
|
|
@ -160,7 +160,6 @@ func TestPublishInitialAppBranchesCreatesAndPreservesProduction(t *testing.T) {
|
||||||
type onboardingManagerFake struct {
|
type onboardingManagerFake struct {
|
||||||
owner string
|
owner string
|
||||||
calls []string
|
calls []string
|
||||||
remoteRevisions map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *onboardingManagerFake) EnsureRepository(repo, _ string) (bool, error) {
|
func (m *onboardingManagerFake) EnsureRepository(repo, _ string) (bool, error) {
|
||||||
|
|
@ -170,9 +169,6 @@ func (m *onboardingManagerFake) EnsureRepository(repo, _ string) (bool, error) {
|
||||||
|
|
||||||
func (m *onboardingManagerFake) RemoteBranchRevision(targetURL, branch string) (string, error) {
|
func (m *onboardingManagerFake) RemoteBranchRevision(targetURL, branch string) (string, error) {
|
||||||
m.calls = append(m.calls, "remote "+targetURL+":"+branch)
|
m.calls = append(m.calls, "remote "+targetURL+":"+branch)
|
||||||
if m.remoteRevisions != nil {
|
|
||||||
return m.remoteRevisions[branch], nil
|
|
||||||
}
|
|
||||||
return "existing", nil
|
return "existing", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,8 +255,6 @@ func TestOnboardAppUsesCanonicalSourceAndCentralClusterManagers(t *testing.T) {
|
||||||
"remote https://git.example.test/Maidn/maidn-e2e-web.git:main",
|
"remote https://git.example.test/Maidn/maidn-e2e-web.git:main",
|
||||||
"remote https://git.example.test/Maidn/maidn-e2e-web.git:production",
|
"remote https://git.example.test/Maidn/maidn-e2e-web.git:production",
|
||||||
"protect Maidn/maidn-e2e-web:production",
|
"protect Maidn/maidn-e2e-web:production",
|
||||||
"remote https://git.example.test/Maidn/maidn-e2e-web.git:maidn/platform-web-ui",
|
|
||||||
"protect Maidn/maidn-e2e-web:maidn/platform-web-ui",
|
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(sourceCalls, want) {
|
if !strings.Contains(sourceCalls, want) {
|
||||||
t.Fatalf("source manager calls = %q, missing %q", sourceCalls, want)
|
t.Fatalf("source manager calls = %q, missing %q", sourceCalls, want)
|
||||||
|
|
@ -269,19 +263,7 @@ func TestOnboardAppUsesCanonicalSourceAndCentralClusterManagers(t *testing.T) {
|
||||||
if strings.Contains(sourceCalls, "delivery") || strings.Contains(sourceCalls, "ensure-pr") || strings.Contains(sourceCalls, "merge-pr") {
|
if strings.Contains(sourceCalls, "delivery") || strings.Contains(sourceCalls, "ensure-pr") || strings.Contains(sourceCalls, "merge-pr") {
|
||||||
t.Fatalf("source manager published a delivery change: %q", sourceCalls)
|
t.Fatalf("source manager published a delivery change: %q", sourceCalls)
|
||||||
}
|
}
|
||||||
sourceRevision := onboardingGit(t, source, "rev-parse", "main")
|
if got := strings.Join(clusterManager.calls, "\n"); got != "register test-org-2/cluster:maidn/register-web-ui" {
|
||||||
if got := strings.Join(clusterManager.calls, "\n"); got != "register test-org-2/cluster:maidn/register-web-ui-"+sourceRevision[:12] {
|
|
||||||
t.Fatalf("cluster manager calls = %q", got)
|
t.Fatalf("cluster manager calls = %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnsurePlatformBranchRequiresExistingBranch(t *testing.T) {
|
|
||||||
manager := &onboardingManagerFake{owner: "Maidn", remoteRevisions: map[string]string{}}
|
|
||||||
err := ensurePlatformBranch(manager, "maidn-e2e-web", "https://git.example.test/Maidn/maidn-e2e-web.git", "web-ui")
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "maidn/platform-web-ui") {
|
|
||||||
t.Fatalf("ensurePlatformBranch() error = %v", err)
|
|
||||||
}
|
|
||||||
if strings.Contains(strings.Join(manager.calls, "\n"), "protect") {
|
|
||||||
t.Fatalf("missing platform branch was protected: %q", manager.calls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,6 @@ type accessToken struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var copyGit = runGit
|
var copyGit = runGit
|
||||||
var hasRemoteBranch = (*RepoManager).HasRemoteBranch
|
|
||||||
|
|
||||||
func (e *APIError) Error() string {
|
func (e *APIError) Error() string {
|
||||||
return fmt.Sprintf("forgejo returned %s", e.Status)
|
return fmt.Sprintf("forgejo returned %s", e.Status)
|
||||||
|
|
@ -516,19 +515,10 @@ func (rm *RepoManager) PublishRepositoryPullRequest(repo, title, branch, base st
|
||||||
return false, errors.New("repository pull request requires distinct non-empty branches")
|
return false, errors.New("repository pull request requires distinct non-empty branches")
|
||||||
}
|
}
|
||||||
repoURL := CloneURL(rm.BaseURL, rm.Owner, repo)
|
repoURL := CloneURL(rm.BaseURL, rm.Owner, repo)
|
||||||
hasBranch, err := hasRemoteBranch(rm, repoURL, branch)
|
hasBranch, err := rm.HasRemoteBranch(repoURL, branch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if hasBranch {
|
|
||||||
open, err := rm.HasOpenPullRequest(repo, branch)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if !open {
|
|
||||||
return false, fmt.Errorf("Forgejo branch %q exists without exactly one open pull request; refusing to reuse it", branch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
temporary, err := os.MkdirTemp("", "maidn-registration-*")
|
temporary, err := os.MkdirTemp("", "maidn-registration-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|
@ -790,10 +780,10 @@ func (rm *RepoManager) TriggerWebhookTest(repo, webhookURL, branch string) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureProtectedBranch disables direct pushes to a managed branch.
|
// EnsureProtectedBranch disables direct pushes to the configured production branch.
|
||||||
func (rm *RepoManager) EnsureProtectedBranch(repo, branch string) error {
|
func (rm *RepoManager) EnsureProtectedBranch(repo, branch string) error {
|
||||||
if repo == "" || branch == "" {
|
if repo == "" || branch == "" {
|
||||||
return errors.New("Forgejo repository and branch are required")
|
return errors.New("Forgejo repository and production branch are required")
|
||||||
}
|
}
|
||||||
endpoint := fmt.Sprintf("%s/api/v1/repos/%s/%s/branch_protections", rm.BaseURL, rm.Owner, repo)
|
endpoint := fmt.Sprintf("%s/api/v1/repos/%s/%s/branch_protections", rm.BaseURL, rm.Owner, repo)
|
||||||
var protections []branchProtection
|
var protections []branchProtection
|
||||||
|
|
@ -811,11 +801,11 @@ func (rm *RepoManager) EnsureProtectedBranch(repo, branch string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(matching) > 1 {
|
if len(matching) > 1 {
|
||||||
return fmt.Errorf("multiple Forgejo branch protections match branch %q", branch)
|
return fmt.Errorf("multiple Forgejo branch protections match production branch %q", branch)
|
||||||
}
|
}
|
||||||
if len(matching) == 1 {
|
if len(matching) == 1 {
|
||||||
if matching[0].EnablePush || matching[0].EnablePushWhitelist {
|
if matching[0].EnablePush || matching[0].EnablePushWhitelist {
|
||||||
return fmt.Errorf("Forgejo branch %q permits direct pushes", branch)
|
return fmt.Errorf("Forgejo production branch %q permits direct pushes", branch)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -400,34 +400,6 @@ func TestHasRemoteBranchReturnsFalseForMissingBranch(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPublishRepositoryPullRequestRejectsExistingBranchWithoutOpenPullRequest(t *testing.T) {
|
|
||||||
original := hasRemoteBranch
|
|
||||||
t.Cleanup(func() { hasRemoteBranch = original })
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
if request.Method != http.MethodGet || request.URL.Path != "/api/v1/repos/owner/cluster/pulls" || request.URL.Query().Get("state") != "open" || request.URL.Query().Get("head") != "maidn/register-web-ui-deadbeefcafe" {
|
|
||||||
t.Fatalf("unexpected pull request lookup: %s %s", request.Method, request.URL.String())
|
|
||||||
}
|
|
||||||
_ = json.NewEncoder(writer).Encode([]pullRequest{})
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
manager := NewRepoManager(server.URL, "test-token", "owner", "user", "", "", "main", "")
|
|
||||||
manager.HTTPClient = server.Client()
|
|
||||||
hasRemoteBranch = func(got *RepoManager, repoURL, branch string) (bool, error) {
|
|
||||||
if got != manager || repoURL != CloneURL(server.URL, "owner", "cluster") || branch != "maidn/register-web-ui-deadbeefcafe" {
|
|
||||||
t.Fatalf("unexpected remote branch lookup: %q %q", repoURL, branch)
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := manager.PublishRepositoryPullRequest("cluster", "register web-ui", "maidn/register-web-ui-deadbeefcafe", "main", func(string) error {
|
|
||||||
t.Fatal("change ran for a stale registration branch")
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "without exactly one open pull request") {
|
|
||||||
t.Fatalf("PublishRepositoryPullRequest() error = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMergePullRequest(t *testing.T) {
|
func TestMergePullRequest(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
switch request.Method {
|
switch request.Method {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue