diff --git a/apiserver/getcachedvhdimages.go b/apiserver/getcachedvhdimages.go new file mode 100644 index 00000000000..5eae7fcaa88 --- /dev/null +++ b/apiserver/getcachedvhdimages.go @@ -0,0 +1,41 @@ +package apiserver + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + + "github.com/Azure/agentbaker/pkg/agent" +) + +const ( + // RoutePathGetCachedVersionsOnVHD the route path to get cached vhd images. + RoutePathGetCachedVersionsOnVHD string = "/getcachedversionsonvhd" +) + +// GetCachedVersionsOnVHD endpoint for getting the current versions of components cached on the vhd. +func (api *APIServer) GetCachedVersionsOnVHD(w http.ResponseWriter, r *http.Request) { + agentBaker, err := agent.NewAgentBaker() + if err != nil { + log.Println(err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + cachedOnVHD, err := agentBaker.GetCachedVersionsOnVHD() + if err != nil { + log.Println(err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + jsonResponse, err := json.Marshal(cachedOnVHD) + if err != nil { + log.Println(err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, string(jsonResponse)) +} diff --git a/apiserver/getdistrosigimageconfig.go b/apiserver/getdistrosigimageconfig.go index 5e9b7dbf567..bbd76ae7685 100644 --- a/apiserver/getdistrosigimageconfig.go +++ b/apiserver/getdistrosigimageconfig.go @@ -6,7 +6,7 @@ import ( "log" "net/http" - agent "github.com/Azure/agentbaker/pkg/agent" + "github.com/Azure/agentbaker/pkg/agent" "github.com/Azure/agentbaker/pkg/agent/datamodel" ) diff --git a/apiserver/getlatestsigimageconfig.go b/apiserver/getlatestsigimageconfig.go index 14c149999fa..eb71d541678 100644 --- a/apiserver/getlatestsigimageconfig.go +++ b/apiserver/getlatestsigimageconfig.go @@ -6,7 +6,7 @@ import ( "log" "net/http" - agent "github.com/Azure/agentbaker/pkg/agent" + "github.com/Azure/agentbaker/pkg/agent" "github.com/Azure/agentbaker/pkg/agent/datamodel" ) diff --git a/apiserver/getnodebootstrapdata.go b/apiserver/getnodebootstrapdata.go index 60b7586dcc4..345e82b7b41 100644 --- a/apiserver/getnodebootstrapdata.go +++ b/apiserver/getnodebootstrapdata.go @@ -8,7 +8,7 @@ import ( "net/http" "time" - agent "github.com/Azure/agentbaker/pkg/agent" + "github.com/Azure/agentbaker/pkg/agent" "github.com/Azure/agentbaker/pkg/agent/datamodel" ) diff --git a/apiserver/routers.go b/apiserver/routers.go index 73afb7e22f3..3510ef17315 100644 --- a/apiserver/routers.go +++ b/apiserver/routers.go @@ -40,6 +40,12 @@ func (api *APIServer) NewRouter() *mux.Router { Name("GetDistroSigImageConfig"). HandlerFunc(api.GetDistroSigImageConfig) + router. + Methods("GET"). + Path(RoutePathGetCachedVersionsOnVHD). + Name("GetCachedVersionsOnVHD"). + HandlerFunc(api.GetCachedVersionsOnVHD) + router.Methods("GET").Path("/healthz").Name("healthz").HandlerFunc(healthz) // global timeout and panic handlers. diff --git a/pkg/agent/bakerapi.go b/pkg/agent/bakerapi.go index 1d540b4af7b..e724406df14 100644 --- a/pkg/agent/bakerapi.go +++ b/pkg/agent/bakerapi.go @@ -16,6 +16,7 @@ type AgentBaker interface { GetNodeBootstrapping(ctx context.Context, config *datamodel.NodeBootstrappingConfiguration) (*datamodel.NodeBootstrapping, error) GetLatestSigImageConfig(sigConfig datamodel.SIGConfig, distro datamodel.Distro, envInfo *datamodel.EnvironmentInfo) (*datamodel.SigImageConfig, error) GetDistroSigImageConfig(sigConfig datamodel.SIGConfig, envInfo *datamodel.EnvironmentInfo) (map[datamodel.Distro]datamodel.SigImageConfig, error) + GetCachedVersionsOnVHD() (*datamodel.CachedOnVHD, error) } type agentBakerImpl struct { @@ -174,3 +175,17 @@ func findSIGImageConfig(sigConfig datamodel.SIGAzureEnvironmentSpecConfig, distr return nil } + +func (agentBaker *agentBakerImpl) GetCachedVersionsOnVHD() (*datamodel.CachedOnVHD, error) { + cached := datamodel.CachedOnVHD{ + CachedFromManifest: datamodel.CachedFromManifest, + CachedFromComponentContainerImages: datamodel.CachedFromComponentContainerImages, + CachedFromComponentDownloadedFiles: datamodel.CachedFromComponentDownloadedFiles, + } + + if datamodel.CachedFromManifest == nil || datamodel.CachedFromComponentContainerImages == nil || datamodel.CachedFromComponentDownloadedFiles == nil { + return &cached, fmt.Errorf("cached versions are not available") + } + + return &cached, nil +} diff --git a/pkg/agent/bakerapi_test.go b/pkg/agent/bakerapi_test.go index b89bd7bb907..c504dc6a866 100644 --- a/pkg/agent/bakerapi_test.go +++ b/pkg/agent/bakerapi_test.go @@ -523,4 +523,38 @@ var _ = Describe("AgentBaker API implementation tests", func() { } }) }) + + Context("GetCachedVersionsOnVHD", func() { + It("should return cached VHD data", func() { + agentBaker, err := NewAgentBaker() + Expect(err).NotTo(HaveOccurred()) + + cachedOnVHD, err := agentBaker.GetCachedVersionsOnVHD() + Expect(err).NotTo(HaveOccurred()) + + manifest, err := datamodel.GetManifest() + Expect(err).NotTo(HaveOccurred()) + component, err := datamodel.GetComponents() + Expect(err).NotTo(HaveOccurred()) + + // The indices are hardcoded based on the current components.json. + // Add new components to the bottom of components.json, or update the indices. + pauseIndx := 2 + azureCNSIndx := 5 + cniPluginIndx := 0 + azureCNIIndx := 1 + + Expect(cachedOnVHD.CachedFromManifest.Runc.Installed["default"]).To(Equal(manifest.Runc.Installed["default"])) + Expect(cachedOnVHD.CachedFromManifest.Runc.Pinned["1804"]).To(Equal(manifest.Runc.Pinned["1804"])) + Expect(cachedOnVHD.CachedFromManifest.Containerd.Pinned["1804"]).To(Equal(manifest.Containerd.Pinned["1804"])) + Expect(cachedOnVHD.CachedFromManifest.Containerd.Edge).To(Equal(manifest.Containerd.Edge)) + Expect(cachedOnVHD.CachedFromManifest.Kubernetes.Versions[0]).To(Equal(manifest.Kubernetes.Versions[0])) + Expect(cachedOnVHD.CachedFromComponentContainerImages["pause"].MultiArchVersions[0]).To(Equal(component.ContainerImages[pauseIndx].MultiArchVersions[0])) + Expect(cachedOnVHD.CachedFromComponentContainerImages["azure-cns"].PrefetchOptimizations[0].Version).To( + Equal(component.ContainerImages[azureCNSIndx].PrefetchOptimizations[0].Version)) + Expect(cachedOnVHD.CachedFromComponentContainerImages["azure-cns"].PrefetchOptimizations[0].Binaries[0]).To(Equal("usr/local/bin/azure-cns")) + Expect(cachedOnVHD.CachedFromComponentDownloadedFiles["cni-plugins"].Versions[0]).To(Equal(component.DownloadFiles[cniPluginIndx].Versions[0])) + Expect(cachedOnVHD.CachedFromComponentDownloadedFiles["azure-cni"].Versions[1]).To(Equal(component.DownloadFiles[azureCNIIndx].Versions[1])) + }) + }) }) diff --git a/pkg/agent/datamodel/helper.go b/pkg/agent/datamodel/helper.go index 483aa0340f2..1715e8cca28 100644 --- a/pkg/agent/datamodel/helper.go +++ b/pkg/agent/datamodel/helper.go @@ -7,6 +7,7 @@ import ( "bufio" "bytes" "fmt" + "net/url" "regexp" "sort" "strings" @@ -93,3 +94,33 @@ func IndentString(original string, spaces int) string { } return out.String() } + +func getContainerImageNameFromURL(downloadURL string) (string, error) { + // example URL "downloadURL": "mcr.microsoft.com/oss/kubernetes/autoscaler/addon-resizer:*", + // getting the data between the last / and the last : + parts := strings.Split(downloadURL, "/") + if len(parts) == 0 || len(parts[len(parts)-1]) == 0 { + return "", fmt.Errorf("container image component URL is not in the expected format: %s", downloadURL) + } + lastPart := parts[len(parts)-1] + component := strings.TrimSuffix(lastPart, ":*") + return component, nil +} + +func getComponentNameFromURL(downloadURL string) (string, error) { + // example URL "downloadURL": "https://acs-mirror.azureedge.net/cni-plugins/v*/binaries", + url, err := url.Parse(downloadURL) // /cni-plugins/v*/binaries + if err != nil { + return "", fmt.Errorf("download file image URL is not in the expected format: %s", downloadURL) + } + urlSplit := strings.Split(url.Path, "/") // ["", cni-plugins, v*, binaries] + componentIndx, minURLSplit := 1, 2 + if len(urlSplit) < minURLSplit { + return "", fmt.Errorf("download file image URL is not in the expected format: %s", downloadURL) + } + componentName := urlSplit[componentIndx] + if componentName == "" { + return "", fmt.Errorf("component name is empty in the URL: %s", downloadURL) + } + return componentName, nil +} diff --git a/pkg/agent/datamodel/helper_test.go b/pkg/agent/datamodel/helper_test.go index c03c4b75287..92289395402 100644 --- a/pkg/agent/datamodel/helper_test.go +++ b/pkg/agent/datamodel/helper_test.go @@ -346,3 +346,81 @@ func TestIndentString(t *testing.T) { }) } } + +func TestGetContainerImageNameFromURL(t *testing.T) { + tests := []struct { + name string + downloadURL string + expected string + expectedErr string + }{ + { + name: "empty URL", + downloadURL: "", + expected: "", + expectedErr: "container image component URL is not in the expected format: ", + }, + { + name: "valid URL", + downloadURL: "mcr.microsoft.com/oss/kubernetes/autoscaler/addon-resizer:*", + expected: "addon-resizer", + expectedErr: "", + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + got, err := getContainerImageNameFromURL(test.downloadURL) + if err != nil { + if test.expectedErr != err.Error() { + t.Fatalf("expected error %s, instead got %s", test.expectedErr, err.Error()) + } + } else { + if test.expected != got { + t.Fatalf("expected %s, instead got %s", test.expected, got) + } + } + }) + } +} + +func TestGetComponentNameFromURL(t *testing.T) { + tests := []struct { + name string + downloadURL string + expected string + expectedErr string + }{ + { + name: "empty URL", + downloadURL: "", + expected: "", + expectedErr: "download file image URL is not in the expected format: ", + }, + { + name: "valid URL", + downloadURL: "https://acs-mirror.azureedge.net/cni-plugins/v*/binaries", + expected: "cni-plugins", + expectedErr: "", + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + got, err := getComponentNameFromURL(test.downloadURL) + if err != nil { + if test.expectedErr != err.Error() { + t.Fatalf("expected error %s, instead got %s", test.expectedErr, err.Error()) + } + } else { + if test.expected != got { + t.Fatalf("expected %s, instead got %s", test.expected, got) + } + } + }) + } +} diff --git a/pkg/agent/datamodel/sig_config.go b/pkg/agent/datamodel/sig_config.go index b10cfb18c19..c9e88c44ecd 100644 --- a/pkg/agent/datamodel/sig_config.go +++ b/pkg/agent/datamodel/sig_config.go @@ -1,9 +1,13 @@ package datamodel import ( + "bytes" _ "embed" "encoding/json" "fmt" + "os" + "path" + "runtime" "strings" ) @@ -12,6 +16,107 @@ const ( AzurePublicCloudSigSubscription string = "109a5e88-712a-48ae-9078-9ca8b3c81345" // AKS VHD ) +//nolint:gochecknoglobals +var ( + CachedFromComponentContainerImages map[string]ContainerImage + CachedFromComponentDownloadedFiles map[string]DownloadFile + CachedFromManifest *Manifest +) + +//nolint:gochecknoinits +func init() { + manifest, err := GetManifest() + if err != nil { + panic(err) + } + processManifest(manifest) + + components, err := GetComponents() + if err != nil { + panic(err) + } + err = processComponents(components) + if err != nil { + panic(err) + } +} + +func GetManifest() (Manifest, error) { + _, filename, _, _ := runtime.Caller(0) + manifestFilePath := "../../../parts/linux/cloud-init/artifacts/manifest.json" + manifest, err := getCachedVersionsFromManifestJSON(path.Join(path.Dir(filename), manifestFilePath)) + if err != nil { + return manifest, err + } + return manifest, nil +} + +func GetComponents() (Components, error) { + _, filename, _, _ := runtime.Caller(0) + componentsFilePath := "../../../vhdbuilder/packer/components.json" + components, err := getCachedVersionsFromComponentsJSON(path.Join(path.Dir(filename), componentsFilePath)) + if err != nil { + return components, err + } + return components, nil +} + +func processManifest(manifest Manifest) { + CachedFromManifest = &Manifest{} + + CachedFromManifest.Kubernetes = manifest.Kubernetes + CachedFromManifest.Runc = manifest.Runc + CachedFromManifest.Containerd = manifest.Containerd + CachedFromManifest.NvidiaContainerRuntime = manifest.NvidiaContainerRuntime + CachedFromManifest.NvidiaDrivers = manifest.NvidiaDrivers +} + +func processComponents(components Components) error { + CachedFromComponentContainerImages = make(map[string]ContainerImage) + CachedFromComponentDownloadedFiles = make(map[string]DownloadFile) + + for _, image := range components.ContainerImages { + componentName, err := getContainerImageNameFromURL(image.DownloadURL) + if err != nil { + return fmt.Errorf("error getting component name from URL: %w", err) + } + CachedFromComponentContainerImages[componentName] = image + } + for _, file := range components.DownloadFiles { + componetName, err := getComponentNameFromURL(file.DownloadURL) + if err != nil { + return fmt.Errorf("error getting component name from URL: %w", err) + } + CachedFromComponentDownloadedFiles[componetName] = file + } + return nil +} + +func getCachedVersionsFromManifestJSON(manifestFilePath string) (Manifest, error) { + data, err := os.ReadFile(manifestFilePath) + if err != nil { + return Manifest{}, fmt.Errorf("error reading manifest file: %w", err) + } + data = bytes.ReplaceAll(data, []byte("#EOF"), []byte("")) + var manifest Manifest + if err = json.Unmarshal(data, &manifest); err != nil { + return Manifest{}, fmt.Errorf("error unmarshalling manifest file: %w", err) + } + return manifest, nil +} + +func getCachedVersionsFromComponentsJSON(componentsFilePath string) (Components, error) { + data, err := os.ReadFile(componentsFilePath) + if err != nil { + return Components{}, fmt.Errorf("error reading components file: %w", err) + } + var components Components + if err = json.Unmarshal(data, &components); err != nil { + return Components{}, fmt.Errorf("error unmarshalling components file: %w", err) + } + return components, nil +} + // SIGAzureEnvironmentSpecConfig is the overall configuration differences in different cloud environments. /* TODO(tonyxu) merge this with AzureEnvironmentSpecConfig from aks-engine(pkg/api/azenvtypes.go) once it's moved into AKS RP. */ @@ -278,8 +383,6 @@ func (d Distro) IsWindowsDistro() bool { } // SigImageConfigTemplate represents the SIG image configuration template. -// -//nolint:musttag // tags can be added if deemed necessary type SigImageConfigTemplate struct { ResourceGroup string Gallery string diff --git a/pkg/agent/datamodel/types.go b/pkg/agent/datamodel/types.go index 8c6c22c097b..577da2acf8c 100644 --- a/pkg/agent/datamodel/types.go +++ b/pkg/agent/datamodel/types.go @@ -818,6 +818,63 @@ type ContainerService struct { Properties *Properties `json:"properties,omitempty"` } +// CachedOnVHD represents the cached components on VHD. +type CachedOnVHD struct { + CachedFromManifest *Manifest `json:"cachedFromManifest"` + CachedFromComponentContainerImages map[string]ContainerImage `json:"cachedFromComponentContainerImages"` + CachedFromComponentDownloadedFiles map[string]DownloadFile `json:"cachedFromComponentDownloadedFiles"` +} + +// Dependency represents fields that occur on manifest.json. +type Dependency struct { + Versions []string `json:"versions"` + Installed map[string]string `json:"installed"` + Pinned map[string]string `json:"pinned"` + Edge string `json:"edge"` +} + +// Manifest represents the manifest.json file. +type Manifest struct { + Containerd Dependency `json:"containerd"` + Runc Dependency `json:"runc"` + NvidiaContainerRuntime Dependency `json:"nvidia-container-runtime"` + NvidiaDrivers Dependency `json:"nvidia-drivers"` + Kubernetes Dependency `json:"kubernetes"` +} + +// Versions of components on manifest.json. +type Versions struct { + Versions []string `json:"versions"` +} + +// Components represents the components.json file. +type Components struct { + ContainerImages []ContainerImage `json:"containerImages"` + DownloadFiles []DownloadFile `json:"downloadFiles"` +} + +// ContainerImage represents fields that occur on components.json. +type ContainerImage struct { + DownloadURL string `json:"downloadURL"` + MultiArchVersions []string `json:"multiArchVersions"` + Amd64OnlyVersions []string `json:"amd64OnlyVersions"` + PrefetchOptimizations []PrefetchOptimization `json:"prefetchOptimizations"` +} + +// PrefetchOptimization represents fields that occur on components.json. +type PrefetchOptimization struct { + Version string `json:"version"` + Binaries []string `json:"binaries"` +} + +// DownloadFile represents DownloadFile fields that occur on components.json. +type DownloadFile struct { + FileName string `json:"fileName"` + DownloadLocation string `json:"downloadLocation"` + DownloadURL string `json:"downloadURL"` + Versions []string `json:"versions"` +} + // IsAKSCustomCloud checks if it's in AKS custom cloud. func (cs *ContainerService) IsAKSCustomCloud() bool { return cs.Properties.CustomCloudEnv != nil && @@ -1638,8 +1695,6 @@ type K8sComponents struct { // GetLatestSigImageConfigRequest describes the input for a GetLatestSigImageConfig HTTP request. // This is mostly a wrapper over existing types so RP doesn't have to manually construct JSON. -// -//nolint:musttag // tags can be added if deemed necessary type GetLatestSigImageConfigRequest struct { SIGConfig SIGConfig SubscriptionID string @@ -1649,8 +1704,6 @@ type GetLatestSigImageConfigRequest struct { } // NodeBootstrappingConfiguration represents configurations for node bootstrapping. -// -//nolint:musttag // tags can be added if deemed necessary type NodeBootstrappingConfiguration struct { ContainerService *ContainerService CloudSpecConfig *AzureEnvironmentSpecConfig @@ -1713,8 +1766,6 @@ const ( ) // NodeBootstrapping represents the custom data, CSE, and OS image info needed for node bootstrapping. -// -//nolint:musttag // tags can be added if deemed necessary type NodeBootstrapping struct { CustomData string CSE string