From 0785589c54e96242ca7896d84c9b3124ecc9e9ea Mon Sep 17 00:00:00 2001 From: Vignesh Goutham Ganesh Date: Tue, 10 Oct 2023 14:50:52 +0530 Subject: [PATCH] Air-gapped Image building changes --- projects/aws/image-builder/README.md | 63 +++++++- projects/aws/image-builder/builder/builder.go | 61 +++++++- .../aws/image-builder/builder/constants.go | 5 + .../aws/image-builder/builder/manifests.go | 126 +++++++++++++++- projects/aws/image-builder/builder/types.go | 13 ++ projects/aws/image-builder/builder/utils.go | 135 +++++++++++++++--- projects/aws/image-builder/cmd/build.go | 54 +++++++ 7 files changed, 427 insertions(+), 30 deletions(-) diff --git a/projects/aws/image-builder/README.md b/projects/aws/image-builder/README.md index 79c72eb4a8..d6e3067bdd 100644 --- a/projects/aws/image-builder/README.md +++ b/projects/aws/image-builder/README.md @@ -213,4 +213,65 @@ sudo snap install yq 5. Run the image builder tool for appropriate release channel ``` image-builder build --os ubuntu --hypervisor nutanix --nutanix-config --release-channel -``` \ No newline at end of file +``` + +### Air Gapped Image Building +Image builder only supports building Ubuntu in an airgapped mode for now. + +1. Create the config json file for respective provider and make sure to include the fields required for airgapped building. + An example of baremetal config json for ubuntu airgapped builds are below + ``` + { + "eksa_build_tooling_repo_url": "https://internal-repos/eks-anywhere-build-tooling.git", + "image_builder_repo_url": "https://internal-repos/image-builder.git", + "private_artifacts_eksd_fqdn": "http://artifactory:8081/artifactory", + "private_artifacts_eksa_fqdn": "http://artifactory:8081/artifactory/EKS-A", + "extra_repos": "/home/airgapped/sources.list", + "disable_public_repos": "true", + "iso_url": "http://artifactory:8081/artifactory/EKS-A/ISO/ubuntu-20.04.1-legacy-server-amd64.iso", + "iso_checksum": "f11bda2f2caed8f420802b59f382c25160b114ccc665dbac9c5046e7fceaced2", + "iso_checksum_type": "sha256" + } + ``` +2. Install pre-requisites required for image builder in the environment or admin machine. + 1. Packer version 1.8.7 + 2. Ansible version 2.11.5 + 3. Packer provisioner goss version 3.1.4 + 4. jq + 5. yq + 6. unzip + 7. make + 8. python3-pip +3. From an environment with internet access run the following command to generate the manifest tarball + ``` + image-builder download manifests + ``` + This will download a eks-a-manifests.tar in the current working directory. This tarball is required for airgapped building. +4. Replicate all the required EKS-D and EKS-A artifacts to the internal artifacts server like artifactory. + Required artifacts are as follows + EKS-D amd64 artifacts for specific release branch + 1. kube-apiserver.tar + 2. kube-scheduler.tar + 3. kube-proxy.tar + 4. kube-controller-manager.tar + 5. etcd.tar + 6. coredns.tar + 7. pause.tar + 8. kubectl + 9. kubeadm + 10. kubelet + 11. etcd-linux-amd64-v.tar.gz + 12. cni-plugins-linux-amd64-v.tar.gz + + + EKS-A amd64 artifacts for specific release bundle version + 1. containerd-linux-amd64.tar.gz + 2. etcdadm-linux-amd64.tar.gz + 3. cri-tools-amd64.tar.gz + + In addition to these EKS-D and EKS-A artifacts please ensure the base ubuntu iso is also hosted internally. + +5. Run image builder in airgapped mode + ``` + image-builder build --os ubuntu --hypervisor baremetal --release-channel 1-27 --air-gapped --baremetal-config baremetal.json --manifest-tarball eks-a-manifests.tar + ``` \ No newline at end of file diff --git a/projects/aws/image-builder/builder/builder.go b/projects/aws/image-builder/builder/builder.go index 1f46f4b17f..a642bd7ce6 100644 --- a/projects/aws/image-builder/builder/builder.go +++ b/projects/aws/image-builder/builder/builder.go @@ -22,8 +22,28 @@ func (b *BuildOptions) BuildImage() { if err != nil { log.Fatalf("Error retrieving current working directory: %v", err) } + if b.AirGapped { + var eksDArtifactsDomain, eksAArtifactsDomain string + switch b.Hypervisor { + case VSphere: + eksDArtifactsDomain = b.VsphereConfig.PrivateServerEksDDomainUrl + eksAArtifactsDomain = b.VsphereConfig.PrivateServerEksADomainUrl + case Baremetal: + eksDArtifactsDomain = b.BaremetalConfig.PrivateServerEksDDomainUrl + eksAArtifactsDomain = b.BaremetalConfig.PrivateServerEksADomainUrl + case CloudStack: + eksDArtifactsDomain = b.CloudstackConfig.PrivateServerEksDDomainUrl + eksAArtifactsDomain = b.CloudstackConfig.PrivateServerEksADomainUrl + case Nutanix: + eksDArtifactsDomain = b.NutanixConfig.PrivateServerEksDDomainUrl + eksAArtifactsDomain = b.NutanixConfig.PrivateServerEksADomainUrl + } + if err = extractAndPrepManifestTarball(b.ManifestTarball, eksDArtifactsDomain, eksAArtifactsDomain); err != nil { + log.Fatalf(err.Error()) + } + } buildToolingRepoPath := getBuildToolingPath(cwd) - detectedEksaVersion, err := prepBuildToolingRepo(buildToolingRepoPath, b.EKSAReleaseVersion, b.Force) + detectedEksaVersion, err := b.prepBuildToolingRepo(buildToolingRepoPath) if err != nil { log.Fatal(err.Error()) } @@ -38,10 +58,14 @@ func (b *BuildOptions) BuildImage() { upstreamImageBuilderProjectPath := filepath.Join(imageBuilderProjectPath, imageBuilderCAPIDirectory) var outputArtifactPath string var outputImageGlob []string + eksAReleaseManifestUrl, err := getEksAReleasesManifestURL(b.AirGapped) + if err != nil { + log.Fatalf(err.Error()) + } commandEnvVars := []string{ fmt.Sprintf("%s=%s", releaseBranchEnvVar, b.ReleaseChannel), fmt.Sprintf("%s=%s", eksAReleaseVersionEnvVar, detectedEksaVersion), - fmt.Sprintf("%s=%s", eksAReleaseManifestURLEnvVar, getEksAReleasesManifestURL()), + fmt.Sprintf("%s=%s", eksAReleaseManifestURLEnvVar, eksAReleaseManifestUrl), } log.Printf("Initiating Image Build\n Image OS: %s\n Image OS Version: %s\n Hypervisor: %s\n Firmware: %s\n", b.Os, b.OsVersion, b.Hypervisor, b.Firmware) @@ -89,6 +113,14 @@ func (b *BuildOptions) BuildImage() { } var outputImageGlobPattern string if b.Hypervisor == VSphere { + if b.AirGapped { + airGapEnvVars, err := getAirGapCmdEnvVars(b.VsphereConfig.ImageBuilderRepoUrl, detectedEksaVersion, b.ReleaseChannel) + if err != nil { + log.Fatalf("Error getting air gapped env variables: %v", err) + } + commandEnvVars = append(commandEnvVars, airGapEnvVars...) + } + // Set proxy on RHSM if available if b.Os == RedHat && b.VsphereConfig.HttpProxy != "" { if err := setRhsmProxy(&b.VsphereConfig.ProxyConfig, &b.VsphereConfig.RhsmConfig); err != nil { @@ -134,6 +166,14 @@ func (b *BuildOptions) BuildImage() { log.Printf("Image Build Successful\n Please find the output artifact at %s\n", outputArtifactPath) } else if b.Hypervisor == Baremetal { + if b.AirGapped { + airGapEnvVars, err := getAirGapCmdEnvVars(b.BaremetalConfig.ImageBuilderRepoUrl, detectedEksaVersion, b.ReleaseChannel) + if err != nil { + log.Fatalf("Error getting air gapped env variables: %v", err) + } + commandEnvVars = append(commandEnvVars, airGapEnvVars...) + } + // Set proxy on RHSM if available if b.Os == RedHat && b.BaremetalConfig.HttpProxy != "" { if err := setRhsmProxy(&b.BaremetalConfig.ProxyConfig, &b.BaremetalConfig.RhsmConfig); err != nil { @@ -187,10 +227,12 @@ func (b *BuildOptions) BuildImage() { } } - // Patch firmware config for tool - upstreamPatchCommand := fmt.Sprintf("make -C %s patch-repo", imageBuilderProjectPath) - if err := executeMakeBuildCommand(upstreamPatchCommand, commandEnvVars...); err != nil { - log.Fatalf("Error executing upstream patch command: %v", err) + if b.AirGapped { + airGapEnvVars, err := getAirGapCmdEnvVars(b.NutanixConfig.ImageBuilderRepoUrl, detectedEksaVersion, b.ReleaseChannel) + if err != nil { + log.Fatalf("Error getting air gapped env variables: %v", err) + } + commandEnvVars = append(commandEnvVars, airGapEnvVars...) } // Read and set the nutanix connection data @@ -227,6 +269,13 @@ func (b *BuildOptions) BuildImage() { log.Printf("Image Build Successful\n Please find the image uploaded under Nutanix Image Service with name %s\n", b.NutanixConfig.ImageName) } else if b.Hypervisor == CloudStack { + if b.AirGapped { + airGapEnvVars, err := getAirGapCmdEnvVars(b.CloudstackConfig.ImageBuilderRepoUrl, detectedEksaVersion, b.ReleaseChannel) + if err != nil { + log.Fatalf("Error getting air gapped env variables: %v", err) + } + commandEnvVars = append(commandEnvVars, airGapEnvVars...) + } // Set proxy on RHSM if available if b.Os == RedHat && b.CloudstackConfig.HttpProxy != "" { if err := setRhsmProxy(&b.CloudstackConfig.ProxyConfig, &b.CloudstackConfig.RhsmConfig); err != nil { diff --git a/projects/aws/image-builder/builder/constants.go b/projects/aws/image-builder/builder/constants.go index 3480d55850..6442b87cb2 100644 --- a/projects/aws/image-builder/builder/constants.go +++ b/projects/aws/image-builder/builder/constants.go @@ -29,10 +29,12 @@ const ( devEksaReleaseManifestURL string = "https://dev-release-assets.eks-anywhere.model-rocket.aws.dev/eks-a-release.yaml" devBranchEksaReleaseManifestURL string = "https://dev-release-assets.eks-anywhere.model-rocket.aws.dev/%s/eks-a-release.yaml" eksDistroProdDomain string = "distro.eks.amazonaws.com" + eksAnywhereAssetsProdDomain string = "anywhere-assets.eks.amazonaws.com" eksDistroManifestFileNameFormat string = "eks-d-%s.yaml" eksAnywhereManifestFileName string = "eks-a-manifest.yaml" eksAnywhereBundlesFileNameFormat string = "eks-a-bundles-%s.yaml" manifestsTarballName string = "eks-a-manifests.tar" + manifestsDirName string = "eks-a-d-manifests" // Environment variables branchNameEnvVar string = "BRANCH_NAME" @@ -41,6 +43,8 @@ const ( releaseBranchEnvVar string = "RELEASE_BRANCH" eksAReleaseVersionEnvVar string = "EKSA_RELEASE_VERSION" eksAReleaseManifestURLEnvVar string = "EKSA_RELEASE_MANIFEST_URL" + eksABundlesURLEnvVar string = "EKSA_BUNDLE_MANIFEST_URL" + eksDManifestURLEnvVar string = "EKSD_MANIFEST_URL" packerAdditionalFilesConfigFileEnvVar string = "PACKER_ADDITIONAL_FILES_VAR_FILES" rhelUsernameEnvVar string = "RHSM_USERNAME" rhelPasswordEnvVar string = "RHSM_PASSWORD" @@ -50,6 +54,7 @@ const ( packerTypeVarFilesEnvVar string = "PACKER_TYPE_VAR_FILES" packerNutanixVarFilesEnvVar string = "PACKER_NUTANIX_VAR_FILES" eksaUseDevReleaseEnvVar string = "EKSA_USE_DEV_RELEASE" + cloneUrlEnvVar string = "CLONE_URL" // Miscellaneous mainBranch string = "main" diff --git a/projects/aws/image-builder/builder/manifests.go b/projects/aws/image-builder/builder/manifests.go index 229f29b6e9..1cad1b3ada 100644 --- a/projects/aws/image-builder/builder/manifests.go +++ b/projects/aws/image-builder/builder/manifests.go @@ -1,10 +1,13 @@ package builder import ( + "archive/tar" "fmt" + "io" "log" "os" "path/filepath" + "strings" releasev1 "github.com/aws/eks-anywhere/release/api/v1alpha1" k8syaml "sigs.k8s.io/yaml" @@ -20,7 +23,7 @@ func (b *BuildOptions) DownloadManifests() error { } buildToolingRepoPath := getBuildToolingPath(cwd) - _, err = prepBuildToolingRepo(buildToolingRepoPath, "", b.Force) + _, err = b.prepBuildToolingRepo(buildToolingRepoPath) if err != nil { return err } @@ -58,6 +61,11 @@ func downloadEKSDManifests(outputPath string) error { return err } + // Create output directory path + if err = os.MkdirAll(outputPath, 0755); err != nil { + return err + } + for branch, number := range eksDReleaseBranchesWithNumber { manifestUrl := fmt.Sprintf("https://%s/kubernetes-%s/kubernetes-%s-eks-%s.yaml", eksDistroProdDomain, branch, branch, number) manifestFileName := fmt.Sprintf(eksDistroManifestFileNameFormat, branch) @@ -73,7 +81,16 @@ func downloadEKSDManifests(outputPath string) error { func downloadEKSAManifests(outputPath string) error { // Download Release manifest - releaseManifestUrl := getEksAReleasesManifestURL() + releaseManifestUrl, err := getEksAReleasesManifestURL(false) + if err != nil { + return err + } + + // Create output directory path + if err = os.MkdirAll(outputPath, 0755); err != nil { + return err + } + releaseManifestPath := filepath.Join(outputPath, eksAnywhereManifestFileName) log.Printf("Downloading eks-a release manifest") if err := downloadFile(releaseManifestPath, releaseManifestUrl); err != nil { @@ -100,3 +117,108 @@ func downloadEKSAManifests(outputPath string) error { } return nil } + +// extractTarball extracts the provided tarball onto the path +func extractTarball(tarball, path string) error { + tarFile, err := os.Open(tarball) + if err != nil { + return err + } + defer tarFile.Close() + + // Create a new directory to extract the tarball into. + if err = os.RemoveAll(path); err != nil { + return err + } + + err = os.MkdirAll(path, 0755) + if err != nil { + return err + } + + // Create a tar reader for the tarball. + tarReader := tar.NewReader(tarFile) + + // Iterate over the tarball's entries. + for { + // Get the next header from the tarball. + header, err := tarReader.Next() + if err == io.EOF { + // We've reached the end of the tarball. + break + } + if err != nil { + return err + } + + // Create a new file for the tar entry. + outFile, err := os.Create(filepath.Join(path, header.Name)) + if err != nil { + return err + } + defer outFile.Close() + + // Copy the tar entry's contents to the new file. + _, err = io.Copy(outFile, tarReader) + if err != nil { + return err + } + + outFile.Chmod(os.FileMode(header.Mode)) + } + + return nil +} + +func extractAndPrepManifestTarball(tarballFile, privateEksDServerDomain, privateEksAServerDomain string) error { + log.Println("Manifest tarball provided, extracting to directory") + manifestDir, err := getManifestRoot() + if err != nil { + return fmt.Errorf("Error retrieving manifest root") + } + if err = extractTarball(tarballFile, manifestDir); err != nil { + return fmt.Errorf("Error extracting tarball: %v", err) + } + + // Replacing eks-a bundles manifest hostname + // These endpoints are used for artifacts like containerd, crictl & etcdadm + // Find all eks-a bundles manifest and replace hostname + files, err := os.ReadDir(manifestDir) + if err != nil { + return err + } + for _, file := range files { + if !file.IsDir() { + // Find EKS-D Manifests and replace prod distro domain with private server's + absFilePath := filepath.Join(manifestDir, file.Name()) + eksDManifestFilePattern := strings.ReplaceAll(eksDistroManifestFileNameFormat, "%s", "*") + eksDMatch, err := filepath.Match(eksDManifestFilePattern, file.Name()) + if err != nil { + return err + } + + if eksDMatch { + log.Printf("Replacing EKS-D domain name in file: %s\n", file.Name()) + if err = replaceStringInFile(absFilePath, fmt.Sprintf("https://%s", eksDistroProdDomain), privateEksDServerDomain); err != nil { + return err + } + } + + // Find EKS-A Bundles Manifests and replace prod anywhere domain with private server's + eksABundlesManifestFilePattern := strings.ReplaceAll(eksAnywhereBundlesFileNameFormat, "%s", "*") + eksAMatch, err := filepath.Match(eksABundlesManifestFilePattern, file.Name()) + if err != nil { + return err + } + + fmt.Printf("File: %s, Match: %s, Pattern: %s\n", file.Name(), eksAMatch, eksABundlesManifestFilePattern) + if eksAMatch { + log.Printf("Replacing EKS-A domain name in file: %s\n", file.Name()) + if err = replaceStringInFile(absFilePath, fmt.Sprintf("https://%s", eksAnywhereAssetsProdDomain), privateEksAServerDomain); err != nil { + return err + } + } + } + } + return nil +} diff --git a/projects/aws/image-builder/builder/types.go b/projects/aws/image-builder/builder/types.go index 9f547d5ea0..b8063268e4 100644 --- a/projects/aws/image-builder/builder/types.go +++ b/projects/aws/image-builder/builder/types.go @@ -47,6 +47,8 @@ type BuildOptions struct { FilesConfig *AdditionalFilesConfig ReleaseChannel string Force bool + AirGapped bool + ManifestTarball string Firmware string EKSAReleaseVersion string } @@ -73,6 +75,7 @@ type VsphereConfig struct { ProxyConfig ExtraPackagesConfig ExtraOverridesConfig + AirGappedConfig } type BaremetalConfig struct { @@ -82,6 +85,7 @@ type BaremetalConfig struct { ProxyConfig ExtraPackagesConfig ExtraOverridesConfig + AirGappedConfig } type CloudstackConfig struct { @@ -91,6 +95,7 @@ type CloudstackConfig struct { ProxyConfig ExtraPackagesConfig ExtraOverridesConfig + AirGappedConfig } type IsoConfig struct { @@ -121,6 +126,7 @@ type NutanixConfig struct { ProxyConfig ExtraPackagesConfig ExtraOverridesConfig + AirGappedConfig } type AMIConfig struct { @@ -171,3 +177,10 @@ type ExtraOverridesConfig struct { DisablePublicRepos string `json:"disable_public_repos,omitempty"` ReenablePublicRepos string `json:"reenable_public_repos,omitempty"` } + +type AirGappedConfig struct { + EksABuildToolingRepoUrl string `json:"eksa_build_tooling_repo_url,omitempty"` + ImageBuilderRepoUrl string `json:"image_builder_repo_url,omitempty"` + PrivateServerEksDDomainUrl string `json:"private_artifacts_eksd_fqdn,omitempty"` + PrivateServerEksADomainUrl string `json:"private_artifacts_eksa_fqdn,omitempty"` +} diff --git a/projects/aws/image-builder/builder/utils.go b/projects/aws/image-builder/builder/utils.go index 6635c34073..02e901024e 100644 --- a/projects/aws/image-builder/builder/utils.go +++ b/projects/aws/image-builder/builder/utils.go @@ -29,19 +29,19 @@ type EksDRelease struct { KubeVersion string `yaml:"kubeVersion"` } -func prepBuildToolingRepo(buildToolingRepoPath, eksAReleaseVersion string, force bool) (string, error) { +func (bo *BuildOptions) prepBuildToolingRepo(buildToolingRepoPath string) (string, error) { // Clone build tooling repo - if force { + if bo.Force { // Clean up build tooling repo in cwd cleanup(buildToolingRepoPath) } - gitCommitFromBundle, detectedEksaVersion, err := getGitCommitFromBundle(eksAReleaseVersion) + gitCommitFromBundle, detectedEksaVersion, err := bo.getGitCommitFromBundle() if err != nil { return "", fmt.Errorf("Error getting git commit from bundle: %v", err) } if codebuild != "true" { - err = cloneRepo(buildToolingRepoUrl, buildToolingRepoPath) + err = cloneRepo(bo.getBuildToolingRepoUrl(), buildToolingRepoPath) if err != nil { return "", fmt.Errorf("Error cloning build tooling repo: %v", err) } @@ -59,8 +59,24 @@ func prepBuildToolingRepo(buildToolingRepoPath, eksAReleaseVersion string, force return detectedEksaVersion, nil } +func (bo *BuildOptions) getBuildToolingRepoUrl() string { + if bo.AirGapped { + switch bo.Hypervisor { + case VSphere: + return bo.VsphereConfig.EksABuildToolingRepoUrl + case Baremetal: + return bo.BaremetalConfig.EksABuildToolingRepoUrl + case Nutanix: + return bo.NutanixConfig.EksABuildToolingRepoUrl + case CloudStack: + return bo.CloudstackConfig.EksABuildToolingRepoUrl + } + } + return buildToolingRepoUrl +} + func cloneRepo(cloneUrl, destination string) error { - log.Println("Cloning eks-anywhere-build-tooling...") + log.Printf("Cloning eks-anywhere-build-tooling from %s...", cloneUrl) cloneRepoCommandSequence := fmt.Sprintf("git clone %s %s", cloneUrl, destination) cmd := exec.Command("bash", "-c", cloneRepoCommandSequence) return execCommandWithStreamOutput(cmd) @@ -182,6 +198,33 @@ func getRepoRoot() (string, error) { return commandOut, nil } +func getManifestRoot() (string, error) { + cwd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("error retrieving current working directory: %v", err) + } + return filepath.Join(cwd, manifestsDirName), nil +} + +func getAirGapCmdEnvVars(cloneUrl, eksAVersion, eksDReleaseBranch string) ([]string, error) { + manifestRoot, err := getManifestRoot() + if err != nil { + return nil, err + } + manifestRoot = fmt.Sprintf("file://%s", manifestRoot) + // EKS-A Manifest file path + eksABundlesFilePath := filepath.Join(manifestRoot, fmt.Sprintf(eksAnywhereBundlesFileNameFormat, eksAVersion)) + cmdEnvVars := []string{fmt.Sprintf("%s=%s", eksABundlesURLEnvVar, eksABundlesFilePath)} + + // EKS-D Manifest file path + eksDManifestFilePath := filepath.Join(manifestRoot, fmt.Sprintf(eksDistroManifestFileNameFormat, eksDReleaseBranch)) + cmdEnvVars = append(cmdEnvVars, fmt.Sprintf("%s=%s", eksDManifestURLEnvVar, eksDManifestFilePath)) + + // Upstream clone url + cmdEnvVars = append(cmdEnvVars, fmt.Sprintf("%s=%s", cloneUrlEnvVar, cloneUrl)) + return cmdEnvVars, nil +} + func SliceContains(s []string, str string) bool { for _, elem := range s { if elem == str { @@ -202,12 +245,31 @@ func execCommand(cmd *exec.Cmd) (string, error) { return commandOutputStr, nil } -func getGitCommitFromBundle(eksaReleaseVersion string) (string, string, error) { - eksAReleasesManifestURL := getEksAReleasesManifestURL() - releasesManifestContents, err := readFileFromURL(eksAReleasesManifestURL) +func (bo *BuildOptions) getGitCommitFromBundle() (string, string, error) { + manifestDirPath, err := getManifestRoot() + if err != nil { + return "", "", err + } + + eksAReleasesManifestURL, err := getEksAReleasesManifestURL(bo.AirGapped) if err != nil { return "", "", err } + var releasesManifestContents []byte + if bo.ManifestTarball != "" { + eksAManifestFile := filepath.Join(manifestDirPath, eksAnywhereManifestFileName) + log.Printf("Reading EKS-A Manifest file: %s", eksAManifestFile) + releasesManifestContents, err = os.ReadFile(eksAManifestFile) + if err != nil { + return "", "", err + } + } else { + log.Printf("Reading EKS-A Manifest file: %s", eksAReleasesManifestURL) + releasesManifestContents, err = readFileFromURL(eksAReleasesManifestURL) + if err != nil { + return "", "", err + } + } releases := &releasev1.Release{} if err = k8syaml.Unmarshal(releasesManifestContents, releases); err != nil { @@ -219,8 +281,8 @@ func getGitCommitFromBundle(eksaReleaseVersion string) (string, string, error) { if os.Getenv(eksaUseDevReleaseEnvVar) == "true" { eksAReleaseVersion = devEksaReleaseVersion log.Printf("EKSA_USE_DEV_RELEASE set to true, using EKS-A dev release version: %s", eksAReleaseVersion) - } else if eksaReleaseVersion != "" { - eksAReleaseVersion = eksaReleaseVersion + } else if bo.EKSAReleaseVersion != "" { + eksAReleaseVersion = bo.EKSAReleaseVersion log.Printf("EKS-A release version provided: %s", eksAReleaseVersion) } else if eksaVersion != "" { eksAReleaseVersion = eksaVersion @@ -253,11 +315,20 @@ func getGitCommitFromBundle(eksaReleaseVersion string) (string, string, error) { if !foundRelease { return "", "", fmt.Errorf("version %s is not a valid EKS-A release", eksAReleaseVersion) } - log.Printf("Fetching git commit from bundle manifest: %s", bundleManifestUrl) - - bundleManifestContents, err := readFileFromURL(bundleManifestUrl) - if err != nil { - return "", "", err + var bundleManifestContents []byte + if bo.ManifestTarball == "" { + log.Printf("Fetching git commit from bundle manifest: %s", bundleManifestUrl) + bundleManifestContents, err = readFileFromURL(bundleManifestUrl) + if err != nil { + return "", "", err + } + } else { + bundleManifestFile := filepath.Join(manifestDirPath, fmt.Sprintf(eksAnywhereBundlesFileNameFormat, eksAReleaseVersion)) + log.Printf("Fetching git commit from bundle manifest: %s", bundleManifestFile) + bundleManifestContents, err = os.ReadFile(bundleManifestFile) + if err != nil { + return "", "", err + } } bundles := &releasev1.Bundles{} @@ -293,13 +364,19 @@ func readFileFromURL(url string) ([]byte, error) { return data, nil } -func getEksAReleasesManifestURL() string { +func getEksAReleasesManifestURL(airgapped bool) (string, error) { if os.Getenv(eksaUseDevReleaseEnvVar) != "true" { if eksaReleaseManifest != "" { - return eksaReleaseManifest + return eksaReleaseManifest, nil } - - return prodEksaReleaseManifestURL + if airgapped { + manifestRoot, err := getManifestRoot() + if err != nil { + return "", nil + } + return fmt.Sprintf("file://%s/%s", manifestRoot, eksAnywhereManifestFileName), nil + } + return prodEksaReleaseManifestURL, nil } // using a dev release, allow branch_name env var to @@ -310,10 +387,10 @@ func getEksAReleasesManifestURL() string { } if branchName != mainBranch { - return fmt.Sprintf(devBranchEksaReleaseManifestURL, branchName) + return fmt.Sprintf(devBranchEksaReleaseManifestURL, branchName), nil } - return devEksaReleaseManifestURL + return devEksaReleaseManifestURL, nil } // setRhsmProxy takes the proxy config, parses it and sets the appropriate config on rhsm config @@ -395,3 +472,19 @@ func createTarball(fileName, path string) error { return nil } + +func replaceStringInFile(filePath, oldString, newString string) error { + fileContents, err := os.ReadFile(filePath) + if err != nil { + return err + } + replacedString := strings.ReplaceAll(string(fileContents), oldString, newString) + if err = os.Remove(filePath); err != nil { + return err + } + err = os.WriteFile(filePath, []byte(replacedString), 0755) + if err != nil { + return err + } + return nil +} diff --git a/projects/aws/image-builder/cmd/build.go b/projects/aws/image-builder/cmd/build.go index 0010492dab..5257462472 100644 --- a/projects/aws/image-builder/cmd/build.go +++ b/projects/aws/image-builder/cmd/build.go @@ -53,6 +53,8 @@ func init() { buildCmd.Flags().BoolVar(&bo.Force, "force", false, "Force flag to clean up leftover files from previous execution") buildCmd.Flags().StringVar(&bo.Firmware, "firmware", "", "Desired firmware for image build. EFI is only supported for Ubuntu OVA and Raw builds.") buildCmd.Flags().StringVar(&bo.EKSAReleaseVersion, "eksa-release", "", "The EKS-A CLI version to build images for") + buildCmd.Flags().StringVar(&bo.ManifestTarball, "manifest-tarball", "", "Path to Image Builder built EKS-D/A manifest tarball") + buildCmd.Flags().BoolVar(&bo.AirGapped, "air-gapped", false, "Flag to instruct image builder to run in air-gapped mode. Requires --manifest-tarball to be set") if err := buildCmd.MarkFlagRequired("os"); err != nil { log.Fatalf("Error marking flag as required: %v", err) } @@ -108,6 +110,20 @@ func ValidateInputs(bo *builder.BuildOptions) error { } } + // Airgapped + if bo.AirGapped { + if bo.ManifestTarball == "" { + log.Fatalf("Please provide --manifest-tarball when running air-gapped builds") + } + + if bo.Os != builder.Ubuntu { + log.Fatalf("Only Ubuntu os is supported for air-gapped builds") + } + if bo.Hypervisor == builder.AMI { + log.Fatalf("AMI hypervisor not supported for air-gapped builds") + } + } + configPath := "" switch bo.Hypervisor { case builder.VSphere: @@ -163,6 +179,10 @@ func ValidateInputs(bo *builder.BuildOptions) error { if err = validateRHSM(bo.Os, &bo.VsphereConfig.RhsmConfig); err != nil { return err } + if err = validateAirGapped(&bo.VsphereConfig.AirGappedConfig, + bo.VsphereConfig.ExtraRepos, bo.VsphereConfig.IsoUrl); err != nil { + return err + } case builder.Baremetal: if err = json.Unmarshal(config, &bo.BaremetalConfig); err != nil { return err @@ -180,6 +200,10 @@ func ValidateInputs(bo *builder.BuildOptions) error { if err = validateRHSM(bo.Os, &bo.BaremetalConfig.RhsmConfig); err != nil { return err } + if err = validateAirGapped(&bo.BaremetalConfig.AirGappedConfig, + bo.BaremetalConfig.ExtraRepos, bo.BaremetalConfig.IsoUrl); err != nil { + return err + } case builder.Nutanix: if err = json.Unmarshal(config, &bo.NutanixConfig); err != nil { return err @@ -194,6 +218,10 @@ func ValidateInputs(bo *builder.BuildOptions) error { if bo.NutanixConfig.NutanixUserName == "" || bo.NutanixConfig.NutanixPassword == "" { log.Fatalf("\"nutanix_username\" and \"nutanix_password\" are required fields in nutanix-config") } + if err = validateAirGapped(&bo.NutanixConfig.AirGappedConfig, + bo.NutanixConfig.ExtraRepos, bo.NutanixConfig.ImageName); err != nil { + return err + } // TODO Validate other fields as well case builder.CloudStack: if err = json.Unmarshal(config, &bo.CloudstackConfig); err != nil { @@ -212,6 +240,10 @@ func ValidateInputs(bo *builder.BuildOptions) error { if err = validateRHSM(bo.Os, &bo.CloudstackConfig.RhsmConfig); err != nil { return err } + if err = validateAirGapped(&bo.CloudstackConfig.AirGappedConfig, + bo.CloudstackConfig.ExtraRepos, bo.CloudstackConfig.IsoUrl); err != nil { + return err + } case builder.AMI: // Default configuration for AMI builds amiFilter := builder.DefaultUbuntu2004AMIFilterName @@ -356,3 +388,25 @@ func validateFirmware(firmware, os, hypervisor string) error { return nil } + +func validateAirGapped(airgappedConfig *builder.AirGappedConfig, extraRepos, isoUrl string) error { + if airgappedConfig.EksABuildToolingRepoUrl == "" { + return fmt.Errorf("eksa_build_tooling_repo_url must be set when using air-gapped mode") + } + if airgappedConfig.ImageBuilderRepoUrl == "" { + return fmt.Errorf("image_builder_repo_url must be set when using air-gapped mode") + } + if extraRepos == "" { + return fmt.Errorf("Please set extra_repos to internal os package repo when using air-gapped mode") + } + if airgappedConfig.PrivateServerEksDDomainUrl == "" { + return fmt.Errorf("Please set private_artifacts_eksd_fqdn to internal artifacts server's eks-d endpoint") + } + if airgappedConfig.PrivateServerEksADomainUrl == "" { + return fmt.Errorf("Please set private_artifacts_eksa_fqdn to internal artifacts server's eks-a endpoint") + } + if isoUrl == "" { + return fmt.Errorf("Please provide iso_url when building in air-gapped mode") + } + return nil +}