diff --git a/api/v1alpha1/buildjob_types.go b/api/v1alpha1/buildjob_types.go index 2004793..7a120ce 100644 --- a/api/v1alpha1/buildjob_types.go +++ b/api/v1alpha1/buildjob_types.go @@ -23,8 +23,9 @@ type ArtifactDestinationType string type BuildJobPhase string const ( - SourceTypeGit SourceType = "git" - SourceTypePVC SourceType = "pvc" + SourceTypeGit SourceType = "git" + SourceTypePVC SourceType = "pvc" + SourceTypeRepo SourceType = "repo" ArtifactDestinationPVC ArtifactDestinationType = "pvc" ArtifactDestinationOCI ArtifactDestinationType = "oci" @@ -66,13 +67,32 @@ type PVCSource struct { Path string `json:"path,omitempty"` } +// RepoSource configures a Google repo-tool manifest sync for AOSP/AAOS builds. +type RepoSource struct { + // +kubebuilder:validation:MinLength=1 + ManifestURL string `json:"manifestUrl"` + // +optional + Branch string `json:"branch,omitempty"` + // +optional + ManifestName string `json:"manifestName,omitempty"` + // MirrorRef is the name of a PVC containing a local repo mirror; when set, + // repo init receives --reference= to avoid re-downloading blobs. + // +optional + MirrorRef string `json:"mirrorRef,omitempty"` + // SyncJobs controls the -j parallelism passed to repo sync. + // +optional + SyncJobs int `json:"syncJobs,omitempty"` +} + type SourceSpec struct { - // +kubebuilder:validation:Enum=git;pvc + // +kubebuilder:validation:Enum=git;pvc;repo Type SourceType `json:"type"` // +optional Git *GitSource `json:"git,omitempty"` // +optional PVC *PVCSource `json:"pvc,omitempty"` + // +optional + Repo *RepoSource `json:"repo,omitempty"` } type TargetSpec struct { diff --git a/api/v1alpha1/labels.go b/api/v1alpha1/labels.go index b7c2509..09cccc5 100644 --- a/api/v1alpha1/labels.go +++ b/api/v1alpha1/labels.go @@ -21,6 +21,6 @@ const ( AnnotationRunAt = "builder.sdv.cloud.redhat.com/run-at" - ManagedByValue = "bob" - DefaultServiceAccount = "pipeline" + ManagedByValue = "bob" + DefaultServiceAccount = "pipeline" ) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 906061b..66179c2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -57,6 +57,16 @@ func (in *PVCSource) DeepCopy() *PVCSource { return out } +func (in *RepoSource) DeepCopyInto(out *RepoSource) { *out = *in } +func (in *RepoSource) DeepCopy() *RepoSource { + if in == nil { + return nil + } + out := new(RepoSource) + in.DeepCopyInto(out) + return out +} + func (in *SourceSpec) DeepCopyInto(out *SourceSpec) { *out = *in if in.Git != nil { @@ -69,6 +79,11 @@ func (in *SourceSpec) DeepCopyInto(out *SourceSpec) { *out = new(PVCSource) **out = **in } + if in.Repo != nil { + in, out := &in.Repo, &out.Repo + *out = new(RepoSource) + **out = **in + } } func (in *SourceSpec) DeepCopy() *SourceSpec { if in == nil { diff --git a/config/crd/bases/builder.sdv.cloud.redhat.com_buildjobs.yaml b/config/crd/bases/builder.sdv.cloud.redhat.com_buildjobs.yaml index 5d00292..73a5f17 100644 --- a/config/crd/bases/builder.sdv.cloud.redhat.com_buildjobs.yaml +++ b/config/crd/bases/builder.sdv.cloud.redhat.com_buildjobs.yaml @@ -60,7 +60,7 @@ spec: properties: type: type: string - enum: [git, pvc] + enum: [git, pvc, repo] git: type: object required: [url] @@ -84,6 +84,21 @@ spec: path: type: string default: / + repo: + type: object + required: [manifestUrl] + properties: + manifestUrl: + type: string + minLength: 1 + branch: + type: string + manifestName: + type: string + mirrorRef: + type: string + syncJobs: + type: integer target: type: object properties: diff --git a/internal/buildapi/server.go b/internal/buildapi/server.go index 601d205..7d6ada2 100644 --- a/internal/buildapi/server.go +++ b/internal/buildapi/server.go @@ -523,18 +523,18 @@ func (s *Server) handleArtifactDownload(w http.ResponseWriter, r *http.Request) func toSummary(bj *buildv1alpha1.BuildJob) BuildJobSummary { summary := BuildJobSummary{ - Name: bj.Name, - Namespace: bj.Namespace, - Phase: string(bj.Status.Phase), - Board: bj.Spec.Target.Board, - Platform: bj.Spec.Target.Platform, - Architecture: bj.Spec.Target.Architecture, - Image: bj.Spec.Toolchain.Image, - CommitSHA: bj.Status.CommitSHA, + Name: bj.Name, + Namespace: bj.Namespace, + Phase: string(bj.Status.Phase), + Board: bj.Spec.Target.Board, + Platform: bj.Spec.Target.Platform, + Architecture: bj.Spec.Target.Architecture, + Image: bj.Spec.Toolchain.Image, + CommitSHA: bj.Status.CommitSHA, ArtifactURI: bj.Status.ArtifactURI, OCIArtifactRef: bj.Status.OCIArtifactRef, OCIArtifactDigest: bj.Status.OCIArtifactDigest, - PipelineRun: bj.Status.CurrentPipelineRun, + PipelineRun: bj.Status.CurrentPipelineRun, } if bj.Spec.Source.Type == buildv1alpha1.SourceTypeGit && bj.Spec.Source.Git != nil { diff --git a/internal/buildapi/types.go b/internal/buildapi/types.go index 7d9946c..46c5ea1 100644 --- a/internal/buildapi/types.go +++ b/internal/buildapi/types.go @@ -17,21 +17,21 @@ package buildapi import buildv1alpha1 "github.com/centos-automotive-suite/bob/api/v1alpha1" type BuildJobSummary struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Phase string `json:"phase"` - Board string `json:"board,omitempty"` - Platform string `json:"platform,omitempty"` - Architecture string `json:"architecture,omitempty"` - Image string `json:"image,omitempty"` - CommitSHA string `json:"commitSHA,omitempty"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Phase string `json:"phase"` + Board string `json:"board,omitempty"` + Platform string `json:"platform,omitempty"` + Architecture string `json:"architecture,omitempty"` + Image string `json:"image,omitempty"` + CommitSHA string `json:"commitSHA,omitempty"` ArtifactURI string `json:"artifactURI,omitempty"` OCIArtifactRef string `json:"ociArtifactRef,omitempty"` OCIArtifactDigest string `json:"ociArtifactDigest,omitempty"` - Stages []StageInfo `json:"stages,omitempty"` - PipelineRun string `json:"pipelineRun,omitempty"` - Source *SourceInfo `json:"source,omitempty"` - Age string `json:"age,omitempty"` + Stages []StageInfo `json:"stages,omitempty"` + PipelineRun string `json:"pipelineRun,omitempty"` + Source *SourceInfo `json:"source,omitempty"` + Age string `json:"age,omitempty"` } type StageInfo struct { diff --git a/internal/controller/buildjob_controller.go b/internal/controller/buildjob_controller.go index dd2d1ff..4fd33cd 100644 --- a/internal/controller/buildjob_controller.go +++ b/internal/controller/buildjob_controller.go @@ -49,11 +49,11 @@ type BuildJobReconciler struct { } const ( - runAtAnnotation = buildv1alpha1.AnnotationRunAt - conditionTypeSucceeded = "Succeeded" - conditionStatusTrue = "True" - conditionStatusFalse = "False" - conditionReasonRunning = "Running" + runAtAnnotation = buildv1alpha1.AnnotationRunAt + conditionTypeSucceeded = "Succeeded" + conditionStatusTrue = "True" + conditionStatusFalse = "False" + conditionReasonRunning = "Running" ) func (r *BuildJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { diff --git a/internal/controller/buildjob_helpers_test.go b/internal/controller/buildjob_helpers_test.go index f5c35d1..6cfc05b 100644 --- a/internal/controller/buildjob_helpers_test.go +++ b/internal/controller/buildjob_helpers_test.go @@ -354,4 +354,3 @@ func TestComputeOCIRef_NilOCI(t *testing.T) { t.Fatalf("expected empty ref for nil OCI, got %q", ref) } } - diff --git a/internal/controller/cache_controller.go b/internal/controller/cache_controller.go index 9c47f7a..e39e02d 100644 --- a/internal/controller/cache_controller.go +++ b/internal/controller/cache_controller.go @@ -85,8 +85,8 @@ func (r *CacheReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl Name: pvcName, Namespace: cache.Namespace, Labels: map[string]string{ - buildv1alpha1.LabelManagedBy: buildv1alpha1.ManagedByValue, - "builder.sdv.cloud.redhat.com/cache": cache.Name, + buildv1alpha1.LabelManagedBy: buildv1alpha1.ManagedByValue, + "builder.sdv.cloud.redhat.com/cache": cache.Name, "builder.sdv.cloud.redhat.com/cache-type": string(cache.Spec.Type), }, }, diff --git a/internal/controller/toolchain_controller.go b/internal/controller/toolchain_controller.go index 0b03bd0..e2dffa2 100644 --- a/internal/controller/toolchain_controller.go +++ b/internal/controller/toolchain_controller.go @@ -206,10 +206,10 @@ echo "Toolchain image pushed: %s" map[string]interface{}{ "name": "build-push", "image": buildahImage, - "securityContext": map[string]interface{}{ - "runAsNonRoot": true, - "runAsUser": int64(1000), - }, + "securityContext": map[string]interface{}{ + "runAsNonRoot": true, + "runAsUser": int64(1000), + }, "script": script, }, }, diff --git a/internal/tekton/pipelinerun_builder.go b/internal/tekton/pipelinerun_builder.go index 60b8201..b1622d1 100644 --- a/internal/tekton/pipelinerun_builder.go +++ b/internal/tekton/pipelinerun_builder.go @@ -26,12 +26,16 @@ const ( TektonAPIVersion = "tekton.dev/v1" PipelineRunKind = "PipelineRun" GitCloneImage = "alpine/git:latest" + RepoInitImage = "python:3.12-slim" OrasImage = "ghcr.io/oras-project/oras:v1.2.0" workspaceName = "shared-workspace" taskWorkspaceName = "ws" taskCopySource = "copy-source" taskClone = "clone" + taskRepoSync = "repo-sync" + mirrorVolumeName = "repo-mirror" + mirrorMountPath = "/mnt/mirror" cacheVolumeName = "bob-cache" DefaultFirmwareMediaType = "application/vnd.auto.firmware.layer.v1" @@ -107,6 +111,11 @@ echo "Copied PVC source to workspace" copyTask := buildPVCCopyTaskSpec(taskCopySource, image, copyScript, envVars, bj.Spec.Source.PVC.ClaimName) tasks = append(tasks, copyTask) prevStage = taskCopySource + + case bj.Spec.Source.Type == buildv1alpha1.SourceTypeRepo && bj.Spec.Source.Repo != nil: + repoTask := buildRepoTaskSpec(bj.Spec.Source.Repo, envVars) + tasks = append(tasks, repoTask) + prevStage = taskRepoSync } for _, stage := range bj.Spec.Stages { @@ -328,6 +337,87 @@ func buildPVCCopyTaskSpec(name, image, script string, envVars []interface{}, cla } } +func buildRepoTaskSpec(src *buildv1alpha1.RepoSource, envVars []interface{}) map[string]interface{} { + syncJobs := src.SyncJobs + if syncJobs <= 0 { + syncJobs = 4 + } + + initArgs := fmt.Sprintf("-u %s", ShellQuote(src.ManifestURL)) + if src.Branch != "" { + initArgs += fmt.Sprintf(" -b %s", ShellQuote(src.Branch)) + } + if src.ManifestName != "" { + initArgs += fmt.Sprintf(" -m %s", ShellQuote(src.ManifestName)) + } + if src.MirrorRef != "" { + initArgs += fmt.Sprintf(" --reference=%s", ShellQuote(mirrorMountPath)) + } + + script := fmt.Sprintf(`#!/bin/sh +set -eu +# Install repo if not present (repo is a Python script, no compiled binary needed) +if ! command -v repo > /dev/null 2>&1; then + pip install --quiet repo +fi +cd $(workspaces.ws.path) +mkdir -p source +cd source +repo init %s +repo sync -j%d +`, initArgs, syncJobs) + + allowPrivEsc := false + + var volumes []interface{} + var volumeMounts []interface{} + if src.MirrorRef != "" { + volumes = append(volumes, map[string]interface{}{ + "name": mirrorVolumeName, + "persistentVolumeClaim": map[string]interface{}{ + "claimName": src.MirrorRef, + "readOnly": true, + }, + }) + volumeMounts = append(volumeMounts, map[string]interface{}{ + "name": mirrorVolumeName, + "mountPath": mirrorMountPath, + "readOnly": true, + }) + } + + step := map[string]interface{}{ + "name": "run", + "image": RepoInitImage, + "env": envVars, + "securityContext": map[string]interface{}{ + "allowPrivilegeEscalation": allowPrivEsc, + }, + "script": script, + } + if len(volumeMounts) > 0 { + step["volumeMounts"] = volumeMounts + } + + taskSpec := map[string]interface{}{ + "workspaces": []interface{}{ + map[string]interface{}{"name": taskWorkspaceName, "mountPath": "/workspace"}, + }, + "steps": []interface{}{step}, + } + if len(volumes) > 0 { + taskSpec["volumes"] = volumes + } + + return map[string]interface{}{ + "name": taskRepoSync, + "taskSpec": taskSpec, + "workspaces": []interface{}{ + map[string]interface{}{"name": taskWorkspaceName, "workspace": workspaceName}, + }, + } +} + func buildTaskSpec(name, image, command string, envVars []interface{}, runAfter string, caches []buildv1alpha1.CacheMount) map[string]interface{} { allowPrivEsc := false step := map[string]interface{}{ @@ -442,11 +532,11 @@ func buildOCIArtifactTask(bj *buildv1alpha1.BuildJob, envVars []interface{}, run ref := fmt.Sprintf("%s:%s", oci.Repository, tag) annotations := map[string]string{ - "org.opencontainers.image.title": bj.Name, - "vnd.auto.target.board": bj.Spec.Target.Board, - "vnd.auto.target.platform": bj.Spec.Target.Platform, - "vnd.auto.target.architecture": bj.Spec.Target.Architecture, - "vnd.auto.build.generation": fmt.Sprintf("%d", bj.Generation), + "org.opencontainers.image.title": bj.Name, + "vnd.auto.target.board": bj.Spec.Target.Board, + "vnd.auto.target.platform": bj.Spec.Target.Platform, + "vnd.auto.target.architecture": bj.Spec.Target.Architecture, + "vnd.auto.build.generation": fmt.Sprintf("%d", bj.Generation), } if bj.Spec.Target.Variant != "" { annotations["vnd.auto.target.variant"] = bj.Spec.Target.Variant @@ -588,6 +678,10 @@ func buildEnvVars(bj *buildv1alpha1.BuildJob) []interface{} { if bj.Spec.Target.Variant != "" { vars = append(vars, map[string]interface{}{"name": "BOB_VARIANT", "value": bj.Spec.Target.Variant}) } + if bj.Spec.Source.Type == buildv1alpha1.SourceTypeRepo && bj.Spec.Source.Repo != nil { + vars = append(vars, map[string]interface{}{"name": "BOB_MANIFEST_URL", "value": bj.Spec.Source.Repo.ManifestURL}) + vars = append(vars, map[string]interface{}{"name": "BOB_MANIFEST_BRANCH", "value": bj.Spec.Source.Repo.Branch}) + } return vars } diff --git a/internal/tekton/pipelinerun_builder_test.go b/internal/tekton/pipelinerun_builder_test.go index ba1d98a..9c85dc9 100644 --- a/internal/tekton/pipelinerun_builder_test.go +++ b/internal/tekton/pipelinerun_builder_test.go @@ -867,6 +867,342 @@ func TestBuildPipelineRun_PVCArtifactStillWorks(t *testing.T) { } } +func TestBuildPipelineRun_RepoSourceTask(t *testing.T) { + bj := &buildv1alpha1.BuildJob{ + ObjectMeta: metav1.ObjectMeta{Name: "aaos-build", Namespace: "builds"}, + Spec: buildv1alpha1.BuildJobSpec{ + Source: buildv1alpha1.SourceSpec{ + Type: buildv1alpha1.SourceTypeRepo, + Repo: &buildv1alpha1.RepoSource{ + ManifestURL: "https://android.googlesource.com/platform/manifest", + Branch: "android-14.0.0_r50", + SyncJobs: 16, + }, + }, + Stages: []buildv1alpha1.NamedStage{ + {Name: "build", StageSpec: buildv1alpha1.StageSpec{Command: "m -j$(nproc)"}}, + }, + }, + } + pr := BuildPipelineRun(bj) + tasks := getPipelineTasks(t, pr) + + if len(tasks) != 2 { + t.Fatalf("expected 2 tasks (repo-sync + build), got %d", len(tasks)) + } + + first := tasks[0].(map[string]interface{}) + if first["name"] != taskRepoSync { + t.Fatalf("expected first task to be %s, got %s", taskRepoSync, first["name"]) + } + + taskSpec := first["taskSpec"].(map[string]interface{}) + steps := taskSpec["steps"].([]interface{}) + step := steps[0].(map[string]interface{}) + + if step["image"] != RepoInitImage { + t.Fatalf("expected repo task to use RepoInitImage (%s), got %v", RepoInitImage, step["image"]) + } + + script := step["script"].(string) + if !strings.Contains(script, "repo init") { + t.Fatalf("expected script to contain 'repo init', got:\n%s", script) + } + if !strings.Contains(script, "repo sync") { + t.Fatalf("expected script to contain 'repo sync', got:\n%s", script) + } + if !strings.Contains(script, "-j16") { + t.Fatalf("expected script to pass -j16 to repo sync, got:\n%s", script) + } + if !strings.Contains(script, "android.googlesource.com") { + t.Fatalf("expected manifest URL in script, got:\n%s", script) + } + if !strings.Contains(script, "android-14.0.0_r50") { + t.Fatalf("expected branch in script, got:\n%s", script) + } +} + +func TestBuildPipelineRun_RepoSourceDefaultSyncJobs(t *testing.T) { + bj := &buildv1alpha1.BuildJob{ + ObjectMeta: metav1.ObjectMeta{Name: "aaos-default-jobs", Namespace: "builds"}, + Spec: buildv1alpha1.BuildJobSpec{ + Source: buildv1alpha1.SourceSpec{ + Type: buildv1alpha1.SourceTypeRepo, + Repo: &buildv1alpha1.RepoSource{ + ManifestURL: "https://example.com/manifest", + }, + }, + Stages: []buildv1alpha1.NamedStage{ + {Name: "build", StageSpec: buildv1alpha1.StageSpec{Command: "make"}}, + }, + }, + } + pr := BuildPipelineRun(bj) + tasks := getPipelineTasks(t, pr) + first := tasks[0].(map[string]interface{}) + taskSpec := first["taskSpec"].(map[string]interface{}) + steps := taskSpec["steps"].([]interface{}) + step := steps[0].(map[string]interface{}) + script := step["script"].(string) + + if !strings.Contains(script, "-j4") { + t.Fatalf("expected default syncJobs of 4, script:\n%s", script) + } +} + +func TestBuildPipelineRun_RepoSourceMirrorMount(t *testing.T) { + bj := &buildv1alpha1.BuildJob{ + ObjectMeta: metav1.ObjectMeta{Name: "aaos-mirror", Namespace: "builds"}, + Spec: buildv1alpha1.BuildJobSpec{ + Source: buildv1alpha1.SourceSpec{ + Type: buildv1alpha1.SourceTypeRepo, + Repo: &buildv1alpha1.RepoSource{ + ManifestURL: "https://android.googlesource.com/platform/manifest", + MirrorRef: "aosp-mirror", + }, + }, + Stages: []buildv1alpha1.NamedStage{ + {Name: "build", StageSpec: buildv1alpha1.StageSpec{Command: "make"}}, + }, + }, + } + pr := BuildPipelineRun(bj) + tasks := getPipelineTasks(t, pr) + + syncTask := tasks[0].(map[string]interface{}) + taskSpec := syncTask["taskSpec"].(map[string]interface{}) + + volumes, ok := taskSpec["volumes"].([]interface{}) + if !ok || len(volumes) == 0 { + t.Fatal("expected volumes for mirror PVC when mirrorRef is set") + } + vol := volumes[0].(map[string]interface{}) + if vol["name"] != mirrorVolumeName { + t.Fatalf("expected volume name %s, got %v", mirrorVolumeName, vol["name"]) + } + pvcSpec := vol["persistentVolumeClaim"].(map[string]interface{}) + if pvcSpec["claimName"] != "aosp-mirror" { + t.Fatalf("expected claimName aosp-mirror, got %v", pvcSpec["claimName"]) + } + if pvcSpec["readOnly"] != true { + t.Fatal("expected mirror PVC to be mounted read-only") + } + + steps := taskSpec["steps"].([]interface{}) + step := steps[0].(map[string]interface{}) + mounts := step["volumeMounts"].([]interface{}) + if len(mounts) == 0 { + t.Fatal("expected volumeMounts for mirror PVC") + } + mount := mounts[0].(map[string]interface{}) + if mount["mountPath"] != mirrorMountPath { + t.Fatalf("expected mountPath %s, got %v", mirrorMountPath, mount["mountPath"]) + } + if mount["readOnly"] != true { + t.Fatal("expected mirror volume mount to be read-only") + } + + script := step["script"].(string) + if !strings.Contains(script, "--reference=") { + t.Fatalf("expected --reference flag in repo init when mirrorRef is set, got:\n%s", script) + } + if !strings.Contains(script, mirrorMountPath) { + t.Fatalf("expected mirror mount path %s in script, got:\n%s", mirrorMountPath, script) + } +} + +func TestBuildPipelineRun_RepoSourceNoMirrorMount(t *testing.T) { + bj := &buildv1alpha1.BuildJob{ + ObjectMeta: metav1.ObjectMeta{Name: "aaos-no-mirror", Namespace: "builds"}, + Spec: buildv1alpha1.BuildJobSpec{ + Source: buildv1alpha1.SourceSpec{ + Type: buildv1alpha1.SourceTypeRepo, + Repo: &buildv1alpha1.RepoSource{ + ManifestURL: "https://android.googlesource.com/platform/manifest", + }, + }, + Stages: []buildv1alpha1.NamedStage{ + {Name: "build", StageSpec: buildv1alpha1.StageSpec{Command: "make"}}, + }, + }, + } + pr := BuildPipelineRun(bj) + tasks := getPipelineTasks(t, pr) + + syncTask := tasks[0].(map[string]interface{}) + taskSpec := syncTask["taskSpec"].(map[string]interface{}) + + if _, has := taskSpec["volumes"]; has { + t.Fatal("expected no volumes when mirrorRef is not set") + } + + steps := taskSpec["steps"].([]interface{}) + step := steps[0].(map[string]interface{}) + if _, has := step["volumeMounts"]; has { + t.Fatal("expected no volumeMounts when mirrorRef is not set") + } +} + +func TestBuildPipelineRun_RepoSourceEnvVars(t *testing.T) { + bj := &buildv1alpha1.BuildJob{ + ObjectMeta: metav1.ObjectMeta{Name: "aaos-env", Namespace: "builds"}, + Spec: buildv1alpha1.BuildJobSpec{ + Source: buildv1alpha1.SourceSpec{ + Type: buildv1alpha1.SourceTypeRepo, + Repo: &buildv1alpha1.RepoSource{ + ManifestURL: "https://android.googlesource.com/platform/manifest", + Branch: "android-14.0.0_r50", + }, + }, + Stages: []buildv1alpha1.NamedStage{ + {Name: "build", StageSpec: buildv1alpha1.StageSpec{Command: "make"}}, + }, + }, + } + pr := BuildPipelineRun(bj) + tasks := getPipelineTasks(t, pr) + + // Check env vars on the build stage (all stages get them) + buildTask := tasks[1].(map[string]interface{}) + taskSpec := buildTask["taskSpec"].(map[string]interface{}) + steps := taskSpec["steps"].([]interface{}) + step := steps[0].(map[string]interface{}) + envs := step["env"].([]interface{}) + + envMap := make(map[string]string) + for _, e := range envs { + m := e.(map[string]interface{}) + envMap[m["name"].(string)] = m["value"].(string) + } + + if envMap["BOB_MANIFEST_URL"] != "https://android.googlesource.com/platform/manifest" { + t.Fatalf("expected BOB_MANIFEST_URL to be set, got %q", envMap["BOB_MANIFEST_URL"]) + } + if envMap["BOB_MANIFEST_BRANCH"] != "android-14.0.0_r50" { + t.Fatalf("expected BOB_MANIFEST_BRANCH=android-14.0.0_r50, got %q", envMap["BOB_MANIFEST_BRANCH"]) + } + if envMap["BOB_SOURCE_TYPE"] != "repo" { + t.Fatalf("expected BOB_SOURCE_TYPE=repo, got %q", envMap["BOB_SOURCE_TYPE"]) + } +} + +func TestBuildPipelineRun_RepoSourceRunAfterChaining(t *testing.T) { + bj := &buildv1alpha1.BuildJob{ + ObjectMeta: metav1.ObjectMeta{Name: "aaos-chain", Namespace: "builds"}, + Spec: buildv1alpha1.BuildJobSpec{ + Source: buildv1alpha1.SourceSpec{ + Type: buildv1alpha1.SourceTypeRepo, + Repo: &buildv1alpha1.RepoSource{ + ManifestURL: "https://android.googlesource.com/platform/manifest", + }, + }, + Stages: []buildv1alpha1.NamedStage{ + {Name: "build", StageSpec: buildv1alpha1.StageSpec{Command: "m -j$(nproc)"}}, + {Name: "package", StageSpec: buildv1alpha1.StageSpec{Command: "cp out/*.img /workspace/"}}, + }, + }, + } + pr := BuildPipelineRun(bj) + tasks := getPipelineTasks(t, pr) + + if len(tasks) != 3 { + t.Fatalf("expected 3 tasks (repo-sync + build + package), got %d", len(tasks)) + } + + syncTask := tasks[0].(map[string]interface{}) + if _, has := syncTask["runAfter"]; has { + t.Fatal("repo-sync should not have runAfter") + } + + buildTask := tasks[1].(map[string]interface{}) + runAfter := buildTask["runAfter"].([]interface{}) + if runAfter[0] != taskRepoSync { + t.Fatalf("build should runAfter %s, got %v", taskRepoSync, runAfter) + } + + packageTask := tasks[2].(map[string]interface{}) + packageRunAfter := packageTask["runAfter"].([]interface{}) + if packageRunAfter[0] != "build" { + t.Fatalf("package should runAfter build, got %v", packageRunAfter) + } +} + +func TestBuildPipelineRun_RepoSourceManifestName(t *testing.T) { + bj := &buildv1alpha1.BuildJob{ + ObjectMeta: metav1.ObjectMeta{Name: "aaos-manifest-name", Namespace: "builds"}, + Spec: buildv1alpha1.BuildJobSpec{ + Source: buildv1alpha1.SourceSpec{ + Type: buildv1alpha1.SourceTypeRepo, + Repo: &buildv1alpha1.RepoSource{ + ManifestURL: "https://android.googlesource.com/platform/manifest", + ManifestName: "default.xml", + }, + }, + Stages: []buildv1alpha1.NamedStage{ + {Name: "build", StageSpec: buildv1alpha1.StageSpec{Command: "make"}}, + }, + }, + } + pr := BuildPipelineRun(bj) + tasks := getPipelineTasks(t, pr) + syncTask := tasks[0].(map[string]interface{}) + taskSpec := syncTask["taskSpec"].(map[string]interface{}) + steps := taskSpec["steps"].([]interface{}) + step := steps[0].(map[string]interface{}) + script := step["script"].(string) + + if !strings.Contains(script, "-m") { + t.Fatalf("expected -m flag for manifestName in repo init, got:\n%s", script) + } + if !strings.Contains(script, "default.xml") { + t.Fatalf("expected manifest name 'default.xml' in script, got:\n%s", script) + } +} + +func TestBuildPipelineRun_RepoSourceNoPipelineResult(t *testing.T) { + bj := &buildv1alpha1.BuildJob{ + ObjectMeta: metav1.ObjectMeta{Name: "aaos-no-result", Namespace: "builds"}, + Spec: buildv1alpha1.BuildJobSpec{ + Source: buildv1alpha1.SourceSpec{ + Type: buildv1alpha1.SourceTypeRepo, + Repo: &buildv1alpha1.RepoSource{ + ManifestURL: "https://android.googlesource.com/platform/manifest", + }, + }, + Stages: []buildv1alpha1.NamedStage{ + {Name: "build", StageSpec: buildv1alpha1.StageSpec{Command: "make"}}, + }, + }, + } + pr := BuildPipelineRun(bj) + spec := pr.Object["spec"].(map[string]interface{}) + ps := spec["pipelineSpec"].(map[string]interface{}) + if _, has := ps["results"]; has { + t.Fatal("repo source should not emit a commit-sha pipeline result") + } +} + +func TestBuildPipelineRun_RepoSourceRepoNilSkipsTask(t *testing.T) { + bj := &buildv1alpha1.BuildJob{ + ObjectMeta: metav1.ObjectMeta{Name: "aaos-nil-repo", Namespace: "builds"}, + Spec: buildv1alpha1.BuildJobSpec{ + Source: buildv1alpha1.SourceSpec{Type: buildv1alpha1.SourceTypeRepo}, + Stages: []buildv1alpha1.NamedStage{ + {Name: "build", StageSpec: buildv1alpha1.StageSpec{Command: "make"}}, + }, + }, + } + pr := BuildPipelineRun(bj) + tasks := getPipelineTasks(t, pr) + if len(tasks) != 1 { + t.Fatalf("expected 1 task (no repo-sync when Repo spec is nil), got %d", len(tasks)) + } + first := tasks[0].(map[string]interface{}) + if first["name"] != "build" { + t.Fatalf("expected first task to be build, got %s", first["name"]) + } +} + func getPipelineTasks(t *testing.T, pr *unstructured.Unstructured) []interface{} { t.Helper() spec := pr.Object["spec"].(map[string]interface{})