diff --git a/README.md b/README.md index b513664..1058aeb 100644 --- a/README.md +++ b/README.md @@ -327,6 +327,12 @@ l8k generate --user-config ./cluster-config.yaml \ The generated config already contains the resolved profile. Pass profile flags to `generate` only when you want to override the saved values. +Every Kubernetes object in the generated bundle carries +`nvidia.kubernetes-launch-kit.version: ` in +`metadata.annotations`. This identifies the Launch Kit release that rendered +the object without replacing annotations already supplied by a profile or a +custom workload manifest. + Apply the generated manifests to the cluster: ```bash @@ -346,6 +352,10 @@ caps the whole apply+reconcile phase end-to-end (e.g. `--deploy-timeout 90m`); without it, deploy polls indefinitely — right for SR-IOV on large clusters where reconciliation can take an hour. +When Launch Kit installs or upgrades the Network Operator chart, its Helm +post-renderer adds the same version annotation to chart-rendered resources. +Helm hooks and chart CRDs are outside Helm's post-renderer stream. + Verify the deployment end-to-end: ```bash diff --git a/pkg/app/launcher.go b/pkg/app/launcher.go index 8c7672b..c4aad9b 100644 --- a/pkg/app/launcher.go +++ b/pkg/app/launcher.go @@ -124,6 +124,7 @@ func (l *Launcher) RunContext(ctx context.Context) error { switch pluginName { case networkoperatorplugin.PluginName: l.plugins[pluginName] = &networkoperatorplugin.NetworkOperatorPlugin{ + LaunchKitVersion: l.options.LaunchKitVersion, Groups: l.options.Groups, GpuType: l.options.GpuType, NodeSelector: parseNodeSelector(l.options.NodeSelector), diff --git a/pkg/cmd/deploy.go b/pkg/cmd/deploy.go index 9459026..b8bc142 100644 --- a/pkg/cmd/deploy.go +++ b/pkg/cmd/deploy.go @@ -79,6 +79,7 @@ is used as the manifest directory.`, Run: func(cmd *cobra.Command, args []string) { runTargetCommand(cmd, target.Deploy, hosttarget.NewDeployAdapter( hosttarget.DeployRequest{ + LaunchKitVersion: Version, Kubeconfig: kubeconfig, DeploymentFiles: deploymentFiles, UserConfig: userConfig, diff --git a/pkg/cmd/generate.go b/pkg/cmd/generate.go index b39a7cb..8842265 100644 --- a/pkg/cmd/generate.go +++ b/pkg/cmd/generate.go @@ -84,6 +84,7 @@ Optionally deploy the generated manifests with --deploy.`, --save-deployment-files ./output`, Run: func(cmd *cobra.Command, args []string) { opts := options.Options{ + LaunchKitVersion: Version, ConfigDir: configDir, UserConfig: userConfig, Fabric: fabric, diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 5978bae..799a3cc 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -159,6 +159,7 @@ Use 'l8k schema' to discover tool capabilities programmatically.`, enabledPlugins := parseEnabledPlugins(enabledPlugins) // Create application options from CLI flags opts := options.Options{ + LaunchKitVersion: Version, LogLevel: logLevel, LogFile: logFile, ConfigDir: configDir, diff --git a/pkg/networkoperatorplugin/annotations.go b/pkg/networkoperatorplugin/annotations.go new file mode 100644 index 0000000..99d5398 --- /dev/null +++ b/pkg/networkoperatorplugin/annotations.go @@ -0,0 +1,109 @@ +// Copyright 2026 NVIDIA CORPORATION & AFFILIATES. +// +// SPDX-License-Identifier: Apache-2.0 + +package networkoperatorplugin + +import ( + "bytes" + "fmt" + "io" + + yaml "gopkg.in/yaml.v3" +) + +const launchKitVersionAnnotation = "nvidia.kubernetes-launch-kit.version" + +type annotationPostRenderer struct { + version string +} + +func (r annotationPostRenderer) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, error) { + if renderedManifests == nil { + return nil, fmt.Errorf("rendered manifests must not be nil") + } + annotated, err := annotateResources(renderedManifests.Bytes(), r.version) + if err != nil { + return nil, err + } + return bytes.NewBuffer(annotated), nil +} + +// annotateResources adds the Launch Kit version annotation to every resource +// in a YAML stream while preserving existing annotations. +func annotateResources(stream []byte, version string) ([]byte, error) { + decoder := yaml.NewDecoder(bytes.NewReader(stream)) + documents := []yaml.Node{} + for { + var document yaml.Node + err := decoder.Decode(&document) + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("decode YAML document %d: %w", len(documents)+1, err) + } + if len(document.Content) == 0 || + (document.Content[0].Kind == yaml.ScalarNode && document.Content[0].Tag == "!!null") { + continue + } + documents = append(documents, document) + } + + for i := range documents { + root := documents[i].Content[0] + if root.Kind != yaml.MappingNode { + return nil, fmt.Errorf("YAML document %d must contain a Kubernetes resource mapping", i+1) + } + metadata, ok := yamlMappingValue(root, "metadata") + if !ok || metadata.Kind != yaml.MappingNode { + return nil, fmt.Errorf("YAML document %d must contain a metadata mapping", i+1) + } + annotations, ok := yamlMappingValue(metadata, "annotations") + if !ok { + annotations = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + metadata.Content = append(metadata.Content, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "annotations"}, + annotations, + ) + } else if annotations.Kind == yaml.ScalarNode && annotations.Tag == "!!null" { + *annotations = yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} + } else if annotations.Kind != yaml.MappingNode { + return nil, fmt.Errorf("YAML document %d metadata.annotations must be a mapping", i+1) + } + setYAMLMappingString(annotations, launchKitVersionAnnotation, version) + } + + var output bytes.Buffer + encoder := yaml.NewEncoder(&output) + encoder.SetIndent(2) + for i := range documents { + if err := encoder.Encode(&documents[i]); err != nil { + return nil, fmt.Errorf("encode annotated YAML document %d: %w", i+1, err) + } + } + if err := encoder.Close(); err != nil { + return nil, fmt.Errorf("close annotated YAML encoder: %w", err) + } + return output.Bytes(), nil +} + +func yamlMappingValue(mapping *yaml.Node, key string) (*yaml.Node, bool) { + for i := 0; mapping != nil && mapping.Kind == yaml.MappingNode && i+1 < len(mapping.Content); i += 2 { + if mapping.Content[i].Value == key { + return mapping.Content[i+1], true + } + } + return nil, false +} + +func setYAMLMappingString(mapping *yaml.Node, key, value string) { + if current, ok := yamlMappingValue(mapping, key); ok { + *current = yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: value, Style: yaml.DoubleQuotedStyle} + return + } + mapping.Content = append(mapping.Content, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key}, + &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: value, Style: yaml.DoubleQuotedStyle}, + ) +} diff --git a/pkg/networkoperatorplugin/annotations_test.go b/pkg/networkoperatorplugin/annotations_test.go new file mode 100644 index 0000000..2554e77 --- /dev/null +++ b/pkg/networkoperatorplugin/annotations_test.go @@ -0,0 +1,49 @@ +// Copyright 2026 NVIDIA CORPORATION & AFFILIATES. +// +// SPDX-License-Identifier: Apache-2.0 + +package networkoperatorplugin + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testLaunchKitVersion = "v26.7.0-test" + +func countVersionAnnotations(t *testing.T, stream []byte, version string) int { + t.Helper() + return strings.Count(string(stream), launchKitVersionAnnotation+`: "`+version+`"`) +} + +func TestAnnotateResources(t *testing.T) { + input := []byte(`apiVersion: example.io/v1 +kind: First +metadata: + name: first + annotations: + existing: keep + nvidia.kubernetes-launch-kit.version: "old" +spec: {} +--- +apiVersion: example.io/v1 +kind: Second +metadata: + name: second +spec: {} +`) + + annotated, err := annotateResources(input, "v26.7.0-rc.1") + require.NoError(t, err) + assert.Equal(t, 2, countVersionAnnotations(t, annotated, "v26.7.0-rc.1")) + assert.Contains(t, string(annotated), "existing: keep") + assert.NotContains(t, string(annotated), `nvidia.kubernetes-launch-kit.version: "old"`) +} + +func TestAnnotateResourcesRejectsMissingMetadata(t *testing.T) { + _, err := annotateResources([]byte("apiVersion: v1\nkind: ConfigMap\n"), "v1.0.0") + require.ErrorContains(t, err, "metadata mapping") +} diff --git a/pkg/networkoperatorplugin/deploy.go b/pkg/networkoperatorplugin/deploy.go index b2430fe..e3f7e03 100644 --- a/pkg/networkoperatorplugin/deploy.go +++ b/pkg/networkoperatorplugin/deploy.go @@ -58,6 +58,9 @@ const defaultHelmInstallTimeout = 10 * time.Minute // callsite readable as Phase 0 (helm install) and the existing four phases // grow more parameters over time. type DeployOptions struct { + // LaunchKitVersion is applied to resources rendered by Helm. + LaunchKitVersion string + // DryRun threads through to server-side dry-run for apply and to // action.Install.DryRun / action.Upgrade.DryRun for helm. DryRun bool @@ -111,6 +114,7 @@ type appliedManifest struct { func (p *NetworkOperatorPlugin) DeployProfile(ctx context.Context, profile *profiles.Profile, kubeClient client.Client, manifestsDir string) error { _ = profile return ApplyManifestsFromDir(ctx, kubeClient, manifestsDir, DeployOptions{ + LaunchKitVersion: p.LaunchKitVersion, DryRun: p.DryRun, OverwriteExisting: p.OverwriteExisting, RestConfig: p.RESTConfig, @@ -752,7 +756,7 @@ func runHelmInstallPhase(ctx context.Context, manifestsDir string, opts DeployOp } } - err = InstallOrUpgrade(ctx, opts.RestConfig, opts.NetworkOperator, valuesYAML, opts.OverwriteExisting, timeout, opts.DryRun) + err = InstallOrUpgrade(ctx, opts.RestConfig, opts.NetworkOperator, valuesYAML, opts.LaunchKitVersion, opts.OverwriteExisting, timeout, opts.DryRun) if err == nil { if opts.DryRun { uiOutput.Success("Dry-run: helm install would create network-operator release in namespace %s", diff --git a/pkg/networkoperatorplugin/helm.go b/pkg/networkoperatorplugin/helm.go index 7f62a1b..bf8f775 100644 --- a/pkg/networkoperatorplugin/helm.go +++ b/pkg/networkoperatorplugin/helm.go @@ -145,6 +145,7 @@ func InstallOrUpgrade( restConfig *rest.Config, cfg *config.NetworkOperatorConfig, valuesYAML []byte, + launchKitVersion string, overwriteExisting bool, timeout time.Duration, dryRun bool, @@ -225,7 +226,7 @@ func InstallOrUpgrade( return chrt, nil } - return installOrUpgradeWithLoader(ctx, actionCfg, loadChart, generated, chartVersion, namespace, overwriteExisting, timeout, dryRun) + return installOrUpgradeWithLoader(ctx, actionCfg, loadChart, generated, chartVersion, namespace, launchKitVersion, overwriteExisting, timeout, dryRun) } // installOrUpgradeWithLoader is the test seam for InstallOrUpgrade: it owns @@ -245,6 +246,7 @@ func installOrUpgradeWithLoader( loadChart func() (*chart.Chart, error), generated map[string]interface{}, chartVersion, namespace string, + launchKitVersion string, overwriteExisting bool, timeout time.Duration, dryRun bool, @@ -278,7 +280,7 @@ func installOrUpgradeWithLoader( if lerr != nil { return lerr } - return runUpgrade(ctx, actionCfg, chrt, generated, chartVersion, namespace, timeout, dryRun) + return runUpgrade(ctx, actionCfg, chrt, generated, chartVersion, namespace, launchKitVersion, timeout, dryRun) } // Values gate — same diff logic the preflight values check @@ -297,14 +299,14 @@ func installOrUpgradeWithLoader( if lerr != nil { return lerr } - return runUpgrade(ctx, actionCfg, chrt, generated, chartVersion, namespace, timeout, dryRun) + return runUpgrade(ctx, actionCfg, chrt, generated, chartVersion, namespace, launchKitVersion, timeout, dryRun) case errors.Is(getErr, driver.ErrReleaseNotFound): chrt, lerr := loadChart() if lerr != nil { return lerr } - return runInstall(ctx, actionCfg, chrt, generated, chartVersion, namespace, timeout, dryRun) + return runInstall(ctx, actionCfg, chrt, generated, chartVersion, namespace, launchKitVersion, timeout, dryRun) default: return pkgerrors.NewClusterError( @@ -321,6 +323,7 @@ func runInstall( chrt *chart.Chart, values map[string]interface{}, chartVersion, namespace string, + launchKitVersion string, timeout time.Duration, dryRun bool, ) error { @@ -332,6 +335,7 @@ func runInstall( inst.Timeout = timeout inst.DryRun = dryRun inst.Version = chartVersion + inst.PostRenderer = annotationPostRenderer{version: launchKitVersion} if _, err := inst.RunWithContext(ctx, chrt, values); err != nil { return pkgerrors.NewDeploymentError( @@ -349,6 +353,7 @@ func runUpgrade( chrt *chart.Chart, values map[string]interface{}, chartVersion, namespace string, + launchKitVersion string, timeout time.Duration, dryRun bool, ) error { @@ -359,6 +364,7 @@ func runUpgrade( upg.Timeout = timeout upg.DryRun = dryRun upg.Version = chartVersion + upg.PostRenderer = annotationPostRenderer{version: launchKitVersion} if _, err := upg.RunWithContext(ctx, helmclient.DefaultReleaseName, chrt, values); err != nil { return pkgerrors.NewDeploymentError( diff --git a/pkg/networkoperatorplugin/helm_test.go b/pkg/networkoperatorplugin/helm_test.go index f22ab38..383b89d 100644 --- a/pkg/networkoperatorplugin/helm_test.go +++ b/pkg/networkoperatorplugin/helm_test.go @@ -123,6 +123,7 @@ func TestInstallOrUpgrade_FreshInstall(t *testing.T) { generated, "0.0.0", "nvidia-network-operator", + testLaunchKitVersion, false, // overwriteExisting 30*time.Second, false, @@ -133,6 +134,7 @@ func TestInstallOrUpgrade_FreshInstall(t *testing.T) { require.NoError(t, err) assert.Equal(t, release.StatusDeployed, rel.Info.Status) assert.Equal(t, generated, rel.Config) + assertReleaseResourcesAnnotated(t, rel) } func TestInstallOrUpgrade_SameValuesNoOp(t *testing.T) { @@ -156,6 +158,7 @@ func TestInstallOrUpgrade_SameValuesNoOp(t *testing.T) { values, // identical to deployed "0.0.0", "nvidia-network-operator", + testLaunchKitVersion, false, 30*time.Second, false, @@ -192,6 +195,7 @@ func TestInstallOrUpgrade_ConflictWithoutOverwrite(t *testing.T) { generated, "0.0.0", "nvidia-network-operator", + testLaunchKitVersion, false, 30*time.Second, false, @@ -220,6 +224,7 @@ func TestInstallOrUpgrade_UpgradeOnOverwrite(t *testing.T) { generated, "0.0.0", "nvidia-network-operator", + testLaunchKitVersion, true, // overwriteExisting 30*time.Second, false, @@ -235,6 +240,13 @@ func TestInstallOrUpgrade_UpgradeOnOverwrite(t *testing.T) { require.NoError(t, err) assert.Equal(t, 2, latest.Version) assert.Equal(t, generated, latest.Config) + assertReleaseResourcesAnnotated(t, latest) +} + +func assertReleaseResourcesAnnotated(t *testing.T, rel *release.Release) { + t.Helper() + require.NotNil(t, rel) + assert.Equal(t, 1, countVersionAnnotations(t, []byte(rel.Manifest), testLaunchKitVersion)) } // (UnmarshalValues + DeepEqualValues tests moved to @@ -274,6 +286,7 @@ func TestInstallOrUpgrade_ChartVersionConflictWithoutOverwrite(t *testing.T) { seededValues, // same values — so only chart version differs "2.0.0", // expected "nvidia-network-operator", + testLaunchKitVersion, false, // no overwrite 30*time.Second, false, @@ -310,6 +323,7 @@ func TestInstallOrUpgrade_ChartVersionUpgradeOnOverwrite(t *testing.T) { seededValues, "2.0.0", // expected version differs "nvidia-network-operator", + testLaunchKitVersion, true, // overwrite 30*time.Second, false, @@ -348,6 +362,7 @@ func TestInstallOrUpgrade_DetectsStuckPendingRelease(t *testing.T) { map[string]interface{}{"k": "v"}, "0.0.0", "nvidia-network-operator", + testLaunchKitVersion, true, // even with overwrite, the stuck gate fires first 30*time.Second, false, @@ -358,7 +373,7 @@ func TestInstallOrUpgrade_DetectsStuckPendingRelease(t *testing.T) { } func TestInstallOrUpgrade_RejectsEmptyCfg(t *testing.T) { - err := InstallOrUpgrade(context.Background(), nil, nil, []byte("nfd: {}\n"), false, 30*time.Second, false) + err := InstallOrUpgrade(context.Background(), nil, nil, []byte("nfd: {}\n"), testLaunchKitVersion, false, 30*time.Second, false) require.Error(t, err) } diff --git a/pkg/networkoperatorplugin/networkoperator.go b/pkg/networkoperatorplugin/networkoperator.go index 047d63f..1de0358 100644 --- a/pkg/networkoperatorplugin/networkoperator.go +++ b/pkg/networkoperatorplugin/networkoperator.go @@ -40,6 +40,9 @@ const ( ) type NetworkOperatorPlugin struct { + // LaunchKitVersion is the version reported by the invoking l8k binary. + LaunchKitVersion string + // Groups is the list of source-group identifiers passed via // `--groups ` (matched case-sensitively against // `cluster-config.yaml`'s `clusterConfig[].identifier`). Empty when diff --git a/pkg/networkoperatorplugin/templates.go b/pkg/networkoperatorplugin/templates.go index da3629a..e331cff 100644 --- a/pkg/networkoperatorplugin/templates.go +++ b/pkg/networkoperatorplugin/templates.go @@ -990,9 +990,36 @@ func (p *NetworkOperatorPlugin) GenerateProfileDeploymentFiles(profile *profiles } } + if err := annotateGeneratedResources(results, p.LaunchKitVersion); err != nil { + return nil, err + } + return results, nil } +// annotateGeneratedResources applies Launch Kit ownership metadata at the +// renderer boundary so every current and future profile resource, including a +// user-supplied workload, receives the release version. values.yaml is Helm +// input rather than a Kubernetes resource and is intentionally excluded. +func annotateGeneratedResources(results map[string]string, launchKitVersion string) error { + filenames := make([]string, 0, len(results)) + for filename := range results { + if filename != helmValuesOutputName { + filenames = append(filenames, filename) + } + } + slices.Sort(filenames) + + for _, filename := range filenames { + annotated, err := annotateResources([]byte(results[filename]), launchKitVersion) + if err != nil { + return fmt.Errorf("annotate generated resource %s: %w", filename, err) + } + results[filename] = string(annotated) + } + return nil +} + // hasEmptyNetworkInterfaceNames returns true if any east-west PF across all // groups has an empty NetworkInterface. This happens when discovery finds // multiple nodes per group and omits device names for safety. In rdma_shared diff --git a/pkg/networkoperatorplugin/version_annotations_test.go b/pkg/networkoperatorplugin/version_annotations_test.go new file mode 100644 index 0000000..3c8aa14 --- /dev/null +++ b/pkg/networkoperatorplugin/version_annotations_test.go @@ -0,0 +1,87 @@ +// Copyright 2026 NVIDIA CORPORATION & AFFILIATES. +// +// SPDX-License-Identifier: Apache-2.0 + +package networkoperatorplugin + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + "github.com/nvidia/k8s-launch-kit/pkg/config" +) + +func TestGeneratedResourcesCarryLaunchKitVersionAnnotation(t *testing.T) { + profilesUnderTest := []struct { + dir string + fabric string + deployment string + spcxVersion string + selectedRelease string + }{ + {dir: "host-device-rdma", fabric: "ethernet", deployment: "host_device"}, + {dir: "ipoib-rdma-shared", fabric: "infiniband", deployment: "rdma_shared"}, + {dir: "macvlan-rdma-shared", fabric: "ethernet", deployment: "rdma_shared"}, + {dir: "sriov-ethernet-rdma", fabric: "ethernet", deployment: "sriov"}, + {dir: "sriov-ib-rdma", fabric: "infiniband", deployment: "sriov"}, + {dir: "spectrum-x-ra2.1", fabric: "ethernet", deployment: "sriov", spcxVersion: "RA2.1", selectedRelease: "26.1"}, + {dir: "spectrum-x-ra2.2", fabric: "ethernet", deployment: "sriov", spcxVersion: "RA2.2", selectedRelease: "26.4"}, + {dir: "spectrum-x", fabric: "ethernet", deployment: "sriov", spcxVersion: "RA2.3", selectedRelease: "26.7"}, + } + + for _, profile := range profilesUnderTest { + profile := profile + t.Run(profile.dir, func(t *testing.T) { + ctrllog.SetLogger(zap.New(zap.UseDevMode(true))) + cfg, err := config.LoadFullConfig( + filepath.Join("testdata", "grouping", "mixed-same-type.yaml"), + ctrllog.Log, + ) + require.NoError(t, err) + cfg.Profile = &config.Profile{ + Fabric: profile.fabric, + Deployment: profile.deployment, + Multirail: true, + } + if profile.spcxVersion != "" { + cfg.NetworkOperator.SelectedRelease = profile.selectedRelease + cfg.Profile.SpectrumX = &config.ProfileSpectrumX{ + Enable: true, + SPCXVersion: profile.spcxVersion, + MultiplaneMode: "swplb", + NumberOfPlanes: 2, + TopologyType: config.SpectrumXTopology2Tier, + TopologyFile: writeSpectrumXTopology(t, cfg, 2), + ConfigMapName: "test-spectrum-x-profile", + Profile: "useSoftwareCCAlgorithm: true\n", + } + } + + rendered, err := (&NetworkOperatorPlugin{LaunchKitVersion: testLaunchKitVersion}).GenerateProfileDeploymentFiles( + loadProfileFromDir(t, profile.dir), + cfg, + ) + require.NoError(t, err) + require.NotEmpty(t, rendered) + + for filename, content := range rendered { + if filename == helmValuesOutputName { + assert.NotContains(t, content, launchKitVersionAnnotation, + "values.yaml is Helm input, not a Kubernetes resource") + continue + } + resourceCount := strings.Count(content, "\nkind:") + require.Positivef(t, resourceCount, "rendered file %s", filename) + assert.Equalf(t, resourceCount, + countVersionAnnotations(t, []byte(content), testLaunchKitVersion), + "every document in %s must carry the Launch Kit version", filename) + } + }) + } +} diff --git a/pkg/options/options.go b/pkg/options/options.go index 94c9a41..4870a8d 100644 --- a/pkg/options/options.go +++ b/pkg/options/options.go @@ -20,6 +20,10 @@ import "time" // Options holds all the configuration parameters for the application type Options struct { + // LaunchKitVersion is the version reported by `l8k version`. The CLI + // passes it through so generated resources can record their producer. + LaunchKitVersion string + // Logging LogLevel string LogFile string // Path to log file (optional) diff --git a/pkg/target/host/deploy.go b/pkg/target/host/deploy.go index 311c00f..b6e6f4f 100644 --- a/pkg/target/host/deploy.go +++ b/pkg/target/host/deploy.go @@ -85,6 +85,7 @@ func (deployRunner) Run(ctx context.Context, request DeployRequest) error { } deployOpts := networkoperatorplugin.DeployOptions{ + LaunchKitVersion: request.LaunchKitVersion, DryRun: request.DryRun, OverwriteExisting: request.OverwriteExisting, RestConfig: restConfig, diff --git a/pkg/target/host/driver.go b/pkg/target/host/driver.go index fd04c5f..d2eeefc 100644 --- a/pkg/target/host/driver.go +++ b/pkg/target/host/driver.go @@ -45,6 +45,7 @@ type LauncherRequest struct { // DeployRequest contains the Host-owned inputs for standalone deployment. type DeployRequest struct { + LaunchKitVersion string Kubeconfig string DeploymentFiles string UserConfig string