diff --git a/api/holodeck/v1alpha1/types.go b/api/holodeck/v1alpha1/types.go index 7e9168585..7c86ed8af 100644 --- a/api/holodeck/v1alpha1/types.go +++ b/api/holodeck/v1alpha1/types.go @@ -367,6 +367,69 @@ type ClusterStatus struct { Phase string `json:"phase,omitempty"` } +// ComponentProvenance tracks how a component was installed. +type ComponentProvenance struct { + // Source is the installation method used (e.g., "package", "runfile", "git", "latest", "release"). + Source string `json:"source"` + + // Version is the installed version (if known). + // +optional + // +optional + + Version string `json:"version,omitempty"` + + // Branch is the package branch or tracked branch. + // +optional + // +optional + + Branch string `json:"branch,omitempty"` + + // Repo is the git repository URL (for git/latest sources). + // +optional + // +optional + + Repo string `json:"repo,omitempty"` + + // Ref is the git reference requested (for git sources). + // +optional + // +optional + + Ref string `json:"ref,omitempty"` + + // Commit is the resolved git commit SHA (for git/latest sources). + // +optional + // +optional + + Commit string `json:"commit,omitempty"` +} + +// ComponentsStatus tracks provisioned component information. +type ComponentsStatus struct { + // Driver tracks the NVIDIA driver installation provenance. + // +optional + // +optional + + Driver *ComponentProvenance `json:"driver,omitempty"` + + // Runtime tracks the container runtime installation provenance. + // +optional + // +optional + + Runtime *ComponentProvenance `json:"runtime,omitempty"` + + // Toolkit tracks the NVIDIA Container Toolkit installation provenance. + // +optional + // +optional + + Toolkit *ComponentProvenance `json:"toolkit,omitempty"` + + // Kubernetes tracks the Kubernetes installation provenance. + // +optional + // +optional + + Kubernetes *ComponentProvenance `json:"kubernetes,omitempty"` +} + // EnvironmentStatus defines the observed state of the infra provider type EnvironmentStatus struct { // +listType=map @@ -383,6 +446,13 @@ type EnvironmentStatus struct { // +optional Cluster *ClusterStatus `json:"cluster,omitempty"` + + // Components tracks provenance information for installed components. + // Populated after provisioning with source, version, and commit details. + // +optional + // +optional + + Components *ComponentsStatus `json:"components,omitempty"` } //+kubebuilder:object:root=true diff --git a/cmd/action/ci/entrypoint.go b/cmd/action/ci/entrypoint.go index b9c0adf53..e4292182a 100644 --- a/cmd/action/ci/entrypoint.go +++ b/cmd/action/ci/entrypoint.go @@ -91,7 +91,7 @@ func entrypoint(log *logger.FunLogger) error { defer p.Client.Close() // nolint: errcheck log.Info("Provisioning \u2699") - if err = p.Run(cfg); err != nil { + if _, err = p.Run(cfg); err != nil { return fmt.Errorf("failed to run provisioner: %w", err) } diff --git a/cmd/cli/create/create.go b/cmd/cli/create/create.go index ada93d73d..396ec084f 100644 --- a/cmd/cli/create/create.go +++ b/cmd/cli/create/create.go @@ -430,7 +430,8 @@ func runSingleNodeProvision(log *logger.FunLogger, opts *options) error { } defer p.Client.Close() // nolint: errcheck - if err = p.Run(opts.cfg); err != nil { + componentsStatus, runErr := p.Run(opts.cfg) + if runErr != nil { // Set degraded condition when provisioning fails opts.cfg.Status.Conditions = []metav1.Condition{ { @@ -438,7 +439,7 @@ func runSingleNodeProvision(log *logger.FunLogger, opts *options) error { Status: metav1.ConditionTrue, LastTransitionTime: metav1.Now(), Reason: "ProvisioningFailed", - Message: fmt.Sprintf("Failed to provision environment: %v", err), + Message: fmt.Sprintf("Failed to provision environment: %v", runErr), }, } data, err := jyaml.MarshalYAML(opts.cfg) @@ -448,11 +449,12 @@ func runSingleNodeProvision(log *logger.FunLogger, opts *options) error { if err := os.WriteFile(opts.cacheFile, data, 0600); err != nil { return fmt.Errorf("failed to update cache file with provisioning status: %w", err) } - return fmt.Errorf("failed to run provisioner: %w", err) + return fmt.Errorf("failed to run provisioner: %w", runErr) } - // Set provisioning status to true after successful provisioning + // Set provisioning status and component provenance after successful provisioning opts.cfg.Labels[instances.InstanceProvisionedLabelKey] = "true" + opts.cfg.Status.Components = componentsStatus data, err := jyaml.MarshalYAML(opts.cfg) if err != nil { return fmt.Errorf("failed to marshal environment: %w", err) diff --git a/cmd/cli/describe/describe.go b/cmd/cli/describe/describe.go index 434c69019..bda87a43d 100644 --- a/cmd/cli/describe/describe.go +++ b/cmd/cli/describe/describe.go @@ -125,15 +125,24 @@ type KernelInfo struct { // NVIDIADriverInfo contains NVIDIA driver configuration type NVIDIADriverInfo struct { Install bool `json:"install" yaml:"install"` + Source string `json:"source,omitempty" yaml:"source,omitempty"` Branch string `json:"branch,omitempty" yaml:"branch,omitempty"` Version string `json:"version,omitempty" yaml:"version,omitempty"` + Repo string `json:"repo,omitempty" yaml:"repo,omitempty"` + Ref string `json:"ref,omitempty" yaml:"ref,omitempty"` + Commit string `json:"commit,omitempty" yaml:"commit,omitempty"` } // ContainerRuntimeInfo contains container runtime configuration type ContainerRuntimeInfo struct { Install bool `json:"install" yaml:"install"` Name string `json:"name" yaml:"name"` + Source string `json:"source,omitempty" yaml:"source,omitempty"` Version string `json:"version,omitempty" yaml:"version,omitempty"` + Repo string `json:"repo,omitempty" yaml:"repo,omitempty"` + Ref string `json:"ref,omitempty" yaml:"ref,omitempty"` + Commit string `json:"commit,omitempty" yaml:"commit,omitempty"` + Branch string `json:"branch,omitempty" yaml:"branch,omitempty"` } // ContainerToolkitInfo contains NVIDIA Container Toolkit configuration @@ -142,6 +151,10 @@ type ContainerToolkitInfo struct { Source string `json:"source,omitempty" yaml:"source,omitempty"` Version string `json:"version,omitempty" yaml:"version,omitempty"` EnableCDI bool `json:"enableCDI" yaml:"enableCDI"` + Repo string `json:"repo,omitempty" yaml:"repo,omitempty"` + Ref string `json:"ref,omitempty" yaml:"ref,omitempty"` + Commit string `json:"commit,omitempty" yaml:"commit,omitempty"` + Branch string `json:"branch,omitempty" yaml:"branch,omitempty"` } // KubernetesInfo contains Kubernetes configuration @@ -150,6 +163,10 @@ type KubernetesInfo struct { Installer string `json:"installer,omitempty" yaml:"installer,omitempty"` Version string `json:"version,omitempty" yaml:"version,omitempty"` Source string `json:"source,omitempty" yaml:"source,omitempty"` + Repo string `json:"repo,omitempty" yaml:"repo,omitempty"` + Ref string `json:"ref,omitempty" yaml:"ref,omitempty"` + Commit string `json:"commit,omitempty" yaml:"commit,omitempty"` + Branch string `json:"branch,omitempty" yaml:"branch,omitempty"` } // StatusInfo contains status and conditions @@ -345,28 +362,87 @@ func (m command) buildDescribeOutput(instance *instances.Instance, env *v1alpha1 } if env.Spec.NVIDIADriver.Install { - output.Components.NVIDIADriver = &NVIDIADriverInfo{ + info := &NVIDIADriverInfo{ Install: true, + Source: "package", Branch: env.Spec.NVIDIADriver.Branch, Version: env.Spec.NVIDIADriver.Version, } + // Merge provenance from status if available + if env.Status.Components != nil && env.Status.Components.Driver != nil { + p := env.Status.Components.Driver + info.Source = p.Source + if p.Repo != "" { + info.Repo = p.Repo + } + if p.Ref != "" { + info.Ref = p.Ref + } + if p.Commit != "" { + info.Commit = p.Commit + } + if p.Version != "" { + info.Version = p.Version + } + if p.Branch != "" { + info.Branch = p.Branch + } + } + output.Components.NVIDIADriver = info } if env.Spec.ContainerRuntime.Install { - output.Components.ContainerRuntime = &ContainerRuntimeInfo{ + info := &ContainerRuntimeInfo{ Install: true, Name: string(env.Spec.ContainerRuntime.Name), + Source: "package", Version: env.Spec.ContainerRuntime.Version, } + if env.Status.Components != nil && env.Status.Components.Runtime != nil { + p := env.Status.Components.Runtime + info.Source = p.Source + if p.Repo != "" { + info.Repo = p.Repo + } + if p.Ref != "" { + info.Ref = p.Ref + } + if p.Commit != "" { + info.Commit = p.Commit + } + if p.Branch != "" { + info.Branch = p.Branch + } + } + output.Components.ContainerRuntime = info } if env.Spec.NVIDIAContainerToolkit.Install { - output.Components.ContainerToolkit = &ContainerToolkitInfo{ + info := &ContainerToolkitInfo{ Install: true, Source: string(env.Spec.NVIDIAContainerToolkit.Source), Version: env.Spec.NVIDIAContainerToolkit.Version, EnableCDI: env.Spec.NVIDIAContainerToolkit.EnableCDI, } + if info.Source == "" { + info.Source = "package" + } + if env.Status.Components != nil && env.Status.Components.Toolkit != nil { + p := env.Status.Components.Toolkit + if p.Repo != "" { + info.Repo = p.Repo + } + if p.Ref != "" { + info.Ref = p.Ref + } + if p.Commit != "" { + info.Commit = p.Commit + } + if p.Branch != "" { + info.Branch = p.Branch + } + } + output.Components.ContainerToolkit = info } if env.Spec.Kubernetes.Install { @@ -374,12 +450,31 @@ func (m command) buildDescribeOutput(instance *instances.Instance, env *v1alpha1 if env.Spec.Kubernetes.Release != nil { k8sVersion = env.Spec.Kubernetes.Release.Version } - output.Components.Kubernetes = &KubernetesInfo{ + info := &KubernetesInfo{ Install: true, Installer: env.Spec.Kubernetes.KubernetesInstaller, Version: k8sVersion, Source: string(env.Spec.Kubernetes.Source), } + if info.Source == "" { + info.Source = "release" + } + if env.Status.Components != nil && env.Status.Components.Kubernetes != nil { + p := env.Status.Components.Kubernetes + if p.Repo != "" { + info.Repo = p.Repo + } + if p.Ref != "" { + info.Ref = p.Ref + } + if p.Commit != "" { + info.Commit = p.Commit + } + if p.Branch != "" { + info.Branch = p.Branch + } + } + output.Components.Kubernetes = info } // Status @@ -425,6 +520,24 @@ func (m command) buildDescribeOutput(instance *instances.Instance, env *v1alpha1 return output } +// formatSourceDetail builds a parenthetical detail string showing source provenance. +// Examples: " (package)", " (git, abc12345)", " (latest, main)" +func formatSourceDetail(source, ref, commit, branch string) string { + if source == "" || source == "package" || source == "release" { + return "" + } + parts := source + switch { + case commit != "": + parts += ", " + commit + case ref != "": + parts += ", " + ref + case branch != "": + parts += ", " + branch + } + return " (" + parts + ")" +} + //nolint:errcheck // stdout writes func (m command) printTableFormat(d *DescribeOutput) error { // Instance Information @@ -497,42 +610,62 @@ func (m command) printTableFormat(d *DescribeOutput) error { fmt.Printf("Kernel: %s\n", d.Components.Kernel.Version) } if d.Components.NVIDIADriver != nil { - version := d.Components.NVIDIADriver.Version + di := d.Components.NVIDIADriver + version := di.Version if version == "" { - version = d.Components.NVIDIADriver.Branch + version = di.Branch } if version == "" { version = "latest" } - fmt.Printf("NVIDIA Driver: %s\n", version) + detail := formatSourceDetail(di.Source, di.Ref, di.Commit, di.Branch) + fmt.Printf("NVIDIA Driver: %s%s\n", version, detail) } if d.Components.ContainerRuntime != nil { - version := d.Components.ContainerRuntime.Version + ri := d.Components.ContainerRuntime + version := ri.Version if version == "" { version = "latest" } - fmt.Printf("Container Runtime: %s (%s)\n", d.Components.ContainerRuntime.Name, version) + detail := formatSourceDetail(ri.Source, ri.Ref, ri.Commit, ri.Branch) + fmt.Printf("Container Runtime: %s %s%s\n", ri.Name, version, detail) } if d.Components.ContainerToolkit != nil { - version := d.Components.ContainerToolkit.Version - if version == "" { - version = d.Components.ContainerToolkit.Source + ti := d.Components.ContainerToolkit + version := ti.Version + if version == "" && ti.Ref != "" { + version = ti.Ref } if version == "" { version = "latest" } cdi := "" - if d.Components.ContainerToolkit.EnableCDI { - cdi = " (CDI enabled)" + if ti.EnableCDI { + cdi = ", CDI" + } + detail := formatSourceDetail(ti.Source, ti.Ref, ti.Commit, ti.Branch) + if cdi != "" && detail != "" { + // Append CDI inside the parentheses + detail = detail[:len(detail)-1] + cdi + ")" + } else if cdi != "" { + detail = " (" + ti.Source + cdi + ")" } - fmt.Printf("Container Toolkit: %s%s\n", version, cdi) + fmt.Printf("Container Toolkit: %s%s\n", version, detail) } if d.Components.Kubernetes != nil { - version := d.Components.Kubernetes.Version + ki := d.Components.Kubernetes + version := ki.Version if version == "" { version = "latest" } - fmt.Printf("Kubernetes: %s (%s)\n", version, d.Components.Kubernetes.Installer) + detail := formatSourceDetail(ki.Source, ki.Ref, ki.Commit, ki.Branch) + if detail == "" { + detail = fmt.Sprintf(" (%s)", ki.Installer) + } else { + // Insert installer into detail + detail = fmt.Sprintf(" (%s, %s", ki.Installer, detail[2:]) + } + fmt.Printf("Kubernetes: %s%s\n", version, detail) } // AWS Resources diff --git a/cmd/cli/update/update.go b/cmd/cli/update/update.go index 07ebb9ba8..39b443748 100644 --- a/cmd/cli/update/update.go +++ b/cmd/cli/update/update.go @@ -371,7 +371,12 @@ func (m *command) runProvision(env *v1alpha1.Environment) error { } defer p.Client.Close() //nolint:errcheck - return p.Run(*env) + componentsStatus, err := p.Run(*env) + if err != nil { + return err + } + env.Status.Components = componentsStatus + return nil } func (m *command) runClusterProvision(env *v1alpha1.Environment) error { diff --git a/pkg/provisioner/cluster.go b/pkg/provisioner/cluster.go index d596a6672..9dc9f6ecb 100644 --- a/pkg/provisioner/cluster.go +++ b/pkg/provisioner/cluster.go @@ -171,7 +171,7 @@ func (cp *ClusterProvisioner) provisionBaseOnAllNodes(nodes []NodeInfo) error { envCopy := cp.Environment.DeepCopy() envCopy.Spec.Kubernetes.Install = false - if err := provisioner.Run(*envCopy); err != nil { + if _, err := provisioner.Run(*envCopy); err != nil { if provisioner.Client != nil { _ = provisioner.Client.Close() } diff --git a/pkg/provisioner/provenance.go b/pkg/provisioner/provenance.go new file mode 100644 index 000000000..f9e2fd2cb --- /dev/null +++ b/pkg/provisioner/provenance.go @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package provisioner + +import ( + "github.com/NVIDIA/holodeck/api/holodeck/v1alpha1" +) + +// BuildComponentsStatus creates a ComponentsStatus from the environment spec. +// This captures what was requested for provisioning (source, version, git refs) +// so the CLI can display provenance information. +func BuildComponentsStatus(env v1alpha1.Environment) *v1alpha1.ComponentsStatus { + cs := &v1alpha1.ComponentsStatus{} + hasComponents := false + + // NVIDIA Driver + // Note: multi-source fields (Source, Package, Runfile, Git) are added in + // Phase 1 (feat/issue-567-driver-sources). Until that merges, we only + // track the legacy Branch/Version package fields. + if env.Spec.NVIDIADriver.Install { + hasComponents = true + d := env.Spec.NVIDIADriver + prov := &v1alpha1.ComponentProvenance{ + Source: "package", + Version: d.Version, + Branch: d.Branch, + } + cs.Driver = prov + } + + // Container Runtime + // Note: multi-source fields (Source, Package, Git, Latest) are added in + // Phase 2 (feat/issue-567-runtime-sources). Until that merges, we only + // track the legacy Name/Version package fields. + if env.Spec.ContainerRuntime.Install { + hasComponents = true + cr := env.Spec.ContainerRuntime + prov := &v1alpha1.ComponentProvenance{ + Source: "package", + Version: cr.Version, + } + cs.Runtime = prov + } + + // NVIDIA Container Toolkit + if env.Spec.NVIDIAContainerToolkit.Install { + hasComponents = true + nct := env.Spec.NVIDIAContainerToolkit + prov := &v1alpha1.ComponentProvenance{ + Source: "package", + } + if string(nct.Source) != "" { + prov.Source = string(nct.Source) + } + + switch prov.Source { + case "package": + if nct.Package != nil { + prov.Version = nct.Package.Version + } else if nct.Version != "" { + prov.Version = nct.Version + } + case "git": + if nct.Git != nil { + prov.Repo = nct.Git.Repo + prov.Ref = nct.Git.Ref + } + case "latest": + if nct.Latest != nil { + prov.Branch = nct.Latest.Track + prov.Repo = nct.Latest.Repo + } + } + cs.Toolkit = prov + } + + // Kubernetes + if env.Spec.Kubernetes.Install { + hasComponents = true + k := env.Spec.Kubernetes + prov := &v1alpha1.ComponentProvenance{ + Source: "release", + } + if string(k.Source) != "" { + prov.Source = string(k.Source) + } + + switch prov.Source { + case "release": + if k.Release != nil { + prov.Version = k.Release.Version + } else if k.KubernetesVersion != "" { + prov.Version = k.KubernetesVersion + } + case "git": + if k.Git != nil { + prov.Repo = k.Git.Repo + prov.Ref = k.Git.Ref + } + case "latest": + if k.Latest != nil { + prov.Branch = k.Latest.Track + prov.Repo = k.Latest.Repo + } + } + cs.Kubernetes = prov + } + + if !hasComponents { + return nil + } + return cs +} diff --git a/pkg/provisioner/provenance_test.go b/pkg/provisioner/provenance_test.go new file mode 100644 index 000000000..37320e9e3 --- /dev/null +++ b/pkg/provisioner/provenance_test.go @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package provisioner + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/NVIDIA/holodeck/api/holodeck/v1alpha1" +) + +func TestBuildComponentsStatus_Empty(t *testing.T) { + env := v1alpha1.Environment{} + cs := BuildComponentsStatus(env) + assert.Nil(t, cs, "no components installed should return nil") +} + +func TestBuildComponentsStatus_DriverPackage(t *testing.T) { + env := v1alpha1.Environment{ + Spec: v1alpha1.EnvironmentSpec{ + NVIDIADriver: v1alpha1.NVIDIADriver{ + Install: true, + Branch: "560", + Version: "560.35.03", + }, + }, + } + cs := BuildComponentsStatus(env) + require.NotNil(t, cs) + require.NotNil(t, cs.Driver) + assert.Equal(t, "package", cs.Driver.Source) + assert.Equal(t, "560", cs.Driver.Branch) + assert.Equal(t, "560.35.03", cs.Driver.Version) + assert.Nil(t, cs.Runtime) + assert.Nil(t, cs.Toolkit) + assert.Nil(t, cs.Kubernetes) +} + +func TestBuildComponentsStatus_RuntimePackage(t *testing.T) { + env := v1alpha1.Environment{ + Spec: v1alpha1.EnvironmentSpec{ + ContainerRuntime: v1alpha1.ContainerRuntime{ + Install: true, + Name: v1alpha1.ContainerRuntimeContainerd, + Version: "1.7.20", + }, + }, + } + cs := BuildComponentsStatus(env) + require.NotNil(t, cs) + require.NotNil(t, cs.Runtime) + assert.Equal(t, "package", cs.Runtime.Source) + assert.Equal(t, "1.7.20", cs.Runtime.Version) +} + +func TestBuildComponentsStatus_ToolkitPackage(t *testing.T) { + env := v1alpha1.Environment{ + Spec: v1alpha1.EnvironmentSpec{ + NVIDIAContainerToolkit: v1alpha1.NVIDIAContainerToolkit{ + Install: true, + Version: "1.17.3-1", + }, + }, + } + cs := BuildComponentsStatus(env) + require.NotNil(t, cs) + require.NotNil(t, cs.Toolkit) + assert.Equal(t, "package", cs.Toolkit.Source) + assert.Equal(t, "1.17.3-1", cs.Toolkit.Version) +} + +func TestBuildComponentsStatus_ToolkitGit(t *testing.T) { + env := v1alpha1.Environment{ + Spec: v1alpha1.EnvironmentSpec{ + NVIDIAContainerToolkit: v1alpha1.NVIDIAContainerToolkit{ + Install: true, + Source: v1alpha1.CTKSourceGit, + Git: &v1alpha1.CTKGitSpec{ + Repo: "https://github.com/NVIDIA/nvidia-container-toolkit.git", + Ref: "v1.17.3", + }, + }, + }, + } + cs := BuildComponentsStatus(env) + require.NotNil(t, cs) + require.NotNil(t, cs.Toolkit) + assert.Equal(t, "git", cs.Toolkit.Source) + assert.Equal(t, "https://github.com/NVIDIA/nvidia-container-toolkit.git", cs.Toolkit.Repo) + assert.Equal(t, "v1.17.3", cs.Toolkit.Ref) +} + +func TestBuildComponentsStatus_ToolkitLatest(t *testing.T) { + env := v1alpha1.Environment{ + Spec: v1alpha1.EnvironmentSpec{ + NVIDIAContainerToolkit: v1alpha1.NVIDIAContainerToolkit{ + Install: true, + Source: v1alpha1.CTKSourceLatest, + Latest: &v1alpha1.CTKLatestSpec{ + Track: "main", + Repo: "https://github.com/NVIDIA/nvidia-container-toolkit.git", + }, + }, + }, + } + cs := BuildComponentsStatus(env) + require.NotNil(t, cs) + require.NotNil(t, cs.Toolkit) + assert.Equal(t, "latest", cs.Toolkit.Source) + assert.Equal(t, "main", cs.Toolkit.Branch) + assert.Equal(t, "https://github.com/NVIDIA/nvidia-container-toolkit.git", cs.Toolkit.Repo) +} + +func TestBuildComponentsStatus_KubernetesRelease(t *testing.T) { + env := v1alpha1.Environment{ + Spec: v1alpha1.EnvironmentSpec{ + Kubernetes: v1alpha1.Kubernetes{ + Install: true, + Release: &v1alpha1.K8sReleaseSpec{ + Version: "v1.31.1", + }, + }, + }, + } + cs := BuildComponentsStatus(env) + require.NotNil(t, cs) + require.NotNil(t, cs.Kubernetes) + assert.Equal(t, "release", cs.Kubernetes.Source) + assert.Equal(t, "v1.31.1", cs.Kubernetes.Version) +} + +func TestBuildComponentsStatus_KubernetesGit(t *testing.T) { + env := v1alpha1.Environment{ + Spec: v1alpha1.EnvironmentSpec{ + Kubernetes: v1alpha1.Kubernetes{ + Install: true, + Source: v1alpha1.K8sSourceGit, + Git: &v1alpha1.K8sGitSpec{ + Ref: "refs/heads/master", + }, + }, + }, + } + cs := BuildComponentsStatus(env) + require.NotNil(t, cs) + require.NotNil(t, cs.Kubernetes) + assert.Equal(t, "git", cs.Kubernetes.Source) + assert.Equal(t, "refs/heads/master", cs.Kubernetes.Ref) +} + +func TestBuildComponentsStatus_KubernetesLegacyVersion(t *testing.T) { + env := v1alpha1.Environment{ + Spec: v1alpha1.EnvironmentSpec{ + Kubernetes: v1alpha1.Kubernetes{ + Install: true, + KubernetesVersion: "v1.30.0", + }, + }, + } + cs := BuildComponentsStatus(env) + require.NotNil(t, cs) + require.NotNil(t, cs.Kubernetes) + assert.Equal(t, "release", cs.Kubernetes.Source) + assert.Equal(t, "v1.30.0", cs.Kubernetes.Version) +} + +func TestBuildComponentsStatus_AllComponents(t *testing.T) { + env := v1alpha1.Environment{ + Spec: v1alpha1.EnvironmentSpec{ + NVIDIADriver: v1alpha1.NVIDIADriver{ + Install: true, + Branch: "575", + }, + ContainerRuntime: v1alpha1.ContainerRuntime{ + Install: true, + Name: v1alpha1.ContainerRuntimeContainerd, + }, + NVIDIAContainerToolkit: v1alpha1.NVIDIAContainerToolkit{ + Install: true, + }, + Kubernetes: v1alpha1.Kubernetes{ + Install: true, + Release: &v1alpha1.K8sReleaseSpec{ + Version: "v1.31.1", + }, + }, + }, + } + cs := BuildComponentsStatus(env) + require.NotNil(t, cs) + assert.NotNil(t, cs.Driver) + assert.NotNil(t, cs.Runtime) + assert.NotNil(t, cs.Toolkit) + assert.NotNil(t, cs.Kubernetes) +} diff --git a/pkg/provisioner/provisioner.go b/pkg/provisioner/provisioner.go index b354b1d80..2c6d52683 100644 --- a/pkg/provisioner/provisioner.go +++ b/pkg/provisioner/provisioner.go @@ -124,10 +124,13 @@ func (p *Provisioner) waitForNodeReboot() error { return nil } -func (p *Provisioner) Run(env v1alpha1.Environment) error { +// Run provisions the environment and returns component provenance status. +// The returned ComponentsStatus captures source/version/commit information +// for each installed component. +func (p *Provisioner) Run(env v1alpha1.Environment) (*v1alpha1.ComponentsStatus, error) { // Validate all user-supplied inputs that will be interpolated into shell scripts if err := templates.ValidateTemplateInputs(env); err != nil { - return fmt.Errorf("template input validation failed: %w", err) + return nil, fmt.Errorf("template input validation failed: %w", err) } dependencies := NewDependencies(&env) @@ -140,13 +143,13 @@ func (p *Provisioner) Run(env v1alpha1.Environment) error { // Check if we need to use legacy mode kubernetes, err := templates.NewKubernetes(env) if err != nil { - return fmt.Errorf("failed to create kubernetes template: %w", err) + return nil, fmt.Errorf("failed to create kubernetes template: %w", err) } // Only create kubeadm config file if not using legacy mode if !kubernetes.UseLegacyInit { if err := p.createKubeAdmConfig(env); err != nil { - return fmt.Errorf("failed to create kubeadm config file: %w", err) + return nil, fmt.Errorf("failed to create kubeadm config file: %w", err) } } } @@ -155,34 +158,34 @@ func (p *Provisioner) Run(env v1alpha1.Environment) error { // Create kind config file if it is provided if env.Spec.Kubernetes.KubernetesInstaller == "kind" && env.Spec.Kubernetes.KindConfig != "" { if err := p.createKindConfig(env); err != nil { - return fmt.Errorf("failed to create kind config file: %w", err) + return nil, fmt.Errorf("failed to create kind config file: %w", err) } } for _, node := range dependencies.Resolve() { // Add script header and common functions to the script if err := addScriptHeader(&p.tpl); err != nil { - return fmt.Errorf("failed to add shebang to the script: %w", err) + return nil, fmt.Errorf("failed to add shebang to the script: %w", err) } // Execute the template for the dependency if err := node(&p.tpl, env); err != nil { - return fmt.Errorf("failed to execute template: %w", err) + return nil, fmt.Errorf("failed to execute template: %w", err) } // Provision the instance if err := p.provision(); err != nil { - return fmt.Errorf("failed to provision: %w", err) + return nil, fmt.Errorf("failed to provision: %w", err) } // If kernel version is specified, wait for the node to reboot if env.Spec.Kernel.Version != "" { if err := p.waitForNodeReboot(); err != nil { - return err + return nil, err } } else { // Reset the connection, this step is needed to make sure some configuration changes take effect // e.g after installing docker, the user needs to be added to the docker group if err := p.resetConnection(); err != nil { - return fmt.Errorf("failed to reset connection: %w", err) + return nil, fmt.Errorf("failed to reset connection: %w", err) } } @@ -190,7 +193,8 @@ func (p *Provisioner) Run(env v1alpha1.Environment) error { p.tpl.Reset() } - return nil + // Build component provenance status from spec + return BuildComponentsStatus(env), nil } // resetConnection resets the ssh connection, and retries if it fails to connect diff --git a/tests/aws_test.go b/tests/aws_test.go index 0b910e87d..90f499476 100644 --- a/tests/aws_test.go +++ b/tests/aws_test.go @@ -141,7 +141,8 @@ var _ = DescribeTable("AWS Environment E2E", p.Client = nil } }() - Expect(p.Run(env)).To(Succeed(), "Failed to provision environment") + _, runErr := p.Run(env) + Expect(runErr).NotTo(HaveOccurred(), "Failed to provision environment") By("Kubernetes Configuration") k8s := state.opts.cfg.Spec.Kubernetes