diff --git a/Makefile b/Makefile index b9eb889..6075cb0 100644 --- a/Makefile +++ b/Makefile @@ -71,10 +71,10 @@ test: .PHONY: cover cover: - go test -v \ + go test \ -race \ -cover -coverprofile=coverage.out \ - ./... + $$(go list ./... | grep -v genpyxis) ### Fail if git diff detects a change. Useful for CI. .PHONY: diff-check diff --git a/internal/ansible/ansible.go b/internal/ansible/ansible.go index db867ff..8ccb2aa 100644 --- a/internal/ansible/ansible.go +++ b/internal/ansible/ansible.go @@ -5,15 +5,18 @@ import ( "errors" "strings" - "sigs.k8s.io/yaml" - "github.com/opdev/productctl/internal/logger" "github.com/opdev/productctl/internal/resource" ) -const AnsibleConnectionLocal = "local" +var ErrNoComponentsDeclared = errors.New("no components declared") -type ExtraVars = map[string]any +const ( + AnsibleConnectionLocal = "local" + InventoryKeyContainer = "container_components" + InventoryKeyOperator = "operator_components" + InventoryKeyHelmChart = "helm_chart_components" +) // HelmChartComponentHostVars contains the variables for a container component type HelmChartComponentHostVars struct { @@ -48,13 +51,12 @@ type ProductMeta struct { // certification targets for each component. func GenerateInventory( ctx context.Context, - product *resource.ProductListingDeclaration, mapping MappingDeclaration, -) ([]byte, error) { +) (map[string]any, error) { L := logger.FromContextOrDiscard(ctx) if !product.HasComponents() { - return nil, errors.New("no components declared") + return nil, ErrNoComponentsDeclared } L.Debug("Checking product for container components") @@ -75,19 +77,19 @@ func GenerateInventory( return nil, err } - inventoryOut := map[string]any{ - "container_components": map[string]any{ + inventory := map[string]any{ + InventoryKeyContainer: map[string]any{ "hosts": containerHosts, }, - "helm_chart_components": map[string]any{ - "hosts": helmHosts, - }, - "operator_components": map[string]any{ + InventoryKeyOperator: map[string]any{ "hosts": operatorHosts, }, + InventoryKeyHelmChart: map[string]any{ + "hosts": helmHosts, + }, } - return yaml.Marshal(inventoryOut) + return inventory, nil } // generateContainerComponentInventory produces an Ansible inventory merging @@ -145,14 +147,16 @@ func generateContainerComponentInventory( Component: cmp, } - if tagConfig.ToolFlags != nil { + // if tagConfig.ToolFlags != nil { + if len(tagConfig.ToolFlags) > 0 { L.Debug("container tag has custom tool flags.") vars.ToolFlags = tagConfig.ToolFlags } // TODO: if it doesn't have a component ID, we should omit it or // throw an error. - name := strings.Join([]string{cmp.ID, stripInvalidHostnameChars(vars.Image)}, "-") + name := strings.Join([]string{cmp.ID, vars.Image}, "-") + name = normalizeContainerHostname(name) hosts[name] = &vars } } @@ -231,7 +235,8 @@ func generateOperatorComponentInventory( // TODO: if it doesn't have a component ID, we should omit it or // throw an error. - name := strings.Join([]string{cmp.ID, stripInvalidHostnameChars(vars.Image)}, "-") + name := strings.Join([]string{cmp.ID, vars.Image}, "-") + name = normalizeContainerHostname(name) hosts[name] = &vars } } @@ -239,13 +244,6 @@ func generateOperatorComponentInventory( return hosts, nil } -// stripInvalidHostnameChars replaces characters expected in container URIs with -// characters safe for ansible hostnames. For example, an image with the URI -// quay.io/example/image:v0.0.1 would become quay.io_example_image_v0.0.1. -func stripInvalidHostnameChars(s string) string { - return strings.Replace(strings.Replace(s, ":", "_", -1), "/", "_", -1) -} - // generateHelmComponentInventory produces the helm components of the Ansible // inventory. func generateHelmComponentInventory( @@ -290,8 +288,8 @@ func generateHelmComponentInventory( Component: cmp, } - // TODO: Find a way to make this hostname normalization easier to read. - name := strings.Join([]string{cmp.ID, stripInvalidHostnameChars(strings.Replace(vars.ChartURI, "https://", "remotechart-", -1))}, "-") + name := strings.Join([]string{cmp.ID, vars.ChartURI}, "-") + name = normalizeChartHostname(name) hosts[name] = &vars } @@ -387,4 +385,40 @@ type OperatorComponentHostVars struct { ToolFlags map[string]any `json:"tool_flags,inline,omitempty"` } +// HostMap map is a representation of an ansible Host map, where a given host +// name corresponds to a collection of variables. type HostMap[T ContainerComponentHostVars | HelmChartComponentHostVars | OperatorComponentHostVars] map[string]*T + +// normalizeStringFn defines a string manipulation to be used in a processing strings. +type normalizeStringFn = func(s string) string + +// normalizeString processes s with all normalizationFns, in order, and returns +// the result. +func normalizeString(s string, normalizationFns ...normalizeStringFn) string { + for _, fn := range normalizationFns { + s = fn(s) + } + + return s +} + +func normalizeContainerHostname(s string) string { + return normalizeString( + s, + // Remove the colon from the tag + func(n string) string { return strings.Replace(n, ":", "_", -1) }, + // Remove slashes from URI + func(n string) string { return strings.Replace(n, "/", "_", -1) }, + ) +} + +func normalizeChartHostname(s string) string { + return normalizeString( + s, + // Strip protocol + func(n string) string { return strings.Replace(n, "https://", "remotechart-", -1) }, + func(n string) string { return strings.Replace(n, "http://", "remotechart-", -1) }, + // Remove remaining slashes from URI + func(n string) string { return strings.Replace(n, "/", "_", -1) }, + ) +} diff --git a/internal/ansible/ansible_suite_test.go b/internal/ansible/ansible_suite_test.go new file mode 100644 index 0000000..e8d3b6a --- /dev/null +++ b/internal/ansible/ansible_suite_test.go @@ -0,0 +1,13 @@ +package ansible_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestAnsible(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Ansible Suite") +} diff --git a/internal/ansible/ansible_test.go b/internal/ansible/ansible_test.go new file mode 100644 index 0000000..57a59bd --- /dev/null +++ b/internal/ansible/ansible_test.go @@ -0,0 +1,474 @@ +package ansible + +import ( + "context" + "fmt" + "maps" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/opdev/productctl/internal/resource" + + "sigs.k8s.io/yaml" +) + +var _ = Describe("Ansible", func() { + var ( + ctx context.Context + listing *resource.ProductListingDeclaration + ) + + BeforeEach(func() { + ctx = context.TODO() + listing = &resource.ProductListingDeclaration{} + }) + + When("generating inventory for all components", func() { + var mapping MappingDeclaration + + BeforeEach(func() { + mapping = MappingDeclaration{ + ContainerComponents: ComponentCertificationConfig[ContainerCertTarget]{}, + HelmChartComponents: ComponentCertificationConfig[HelmCertTarget]{}, + OperatorComponents: ComponentCertificationConfig[OperatorCertTarget]{}, + } + }) + + When("the product listing has no componenents defined", func() { + It("should return an error", func() { + _, err := GenerateInventory(ctx, listing, mapping) + Expect(err).To(MatchError(ErrNoComponentsDeclared)) + }) + }) + + When("valid components of each type are defined in the listing", func() { + var container, operator, helmchart resource.Component + BeforeEach(func() { + container = resource.Component{ + ID: "container", + Type: resource.ComponentTypeContainer, + Container: &resource.ContainerComponent{ + Type: resource.ComponentTypeContainer, + OSContentType: resource.ContentTypeUBI, + }, + } + operator = resource.Component{ + ID: "operator", + Type: resource.ComponentTypeContainer, + Container: &resource.ContainerComponent{ + Type: resource.ComponentTypeContainer, + OSContentType: resource.ContentTypeOperatorBundle, + DistributionMethod: resource.ContainerDistributionExternal, + }, + } + + helmchart = resource.Component{ + ID: "helmchart", + Type: resource.ComponentTypeHelmChart, + HelmChart: &resource.HelmChartComponent{ + ChartName: "placeholder-chart", + }, + } + + listing.With.Components = []*resource.Component{ + &container, + &operator, + &helmchart, + } + }) + When("valid component mappings exist for each component", func() { + BeforeEach(func() { + mapping.ContainerComponents[container.ID] = ContainerCertTarget{ + ImageRef: "example.com/example/image", + Tags: []ContainerTagAndOptions{{Tag: "tag"}}, + } + mapping.OperatorComponents[operator.ID] = OperatorCertTarget{ + ImageRef: "example.com/example/image", + Tags: []OperatorTagandOptions{{Tag: "tag"}}, + } + mapping.HelmChartComponents[helmchart.ID] = HelmCertTarget{ + ChartURI: "https://example.com/path/to/chart.0.0.1.tgz", + } + }) + }) + It("should be valid YAML", func() { + inventory, err := GenerateInventory(ctx, listing, mapping) + Expect(err).ToNot(HaveOccurred()) + Expect(inventory[InventoryKeyContainer]).To(HaveLen(1)) + Expect(inventory[InventoryKeyOperator]).To(HaveLen(1)) + Expect(inventory[InventoryKeyHelmChart]).To(HaveLen(1)) + + b, err := yaml.Marshal(inventory) + Expect(err).ToNot(HaveOccurred()) + Expect(b).ToNot(BeEmpty()) + }) + }) + }) + + When("generating inventory for container components", func() { + var certTarget ComponentCertificationConfig[ContainerCertTarget] + + BeforeEach(func() { + certTarget = ComponentCertificationConfig[ContainerCertTarget]{} + }) + + When("the product contains no container components", func() { + It("should produce an empty container component host map", func() { + hostmap, err := generateContainerComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(0)) + }) + }) + + When("the product contains a valid application container component", func() { + var validApplicationContainerComponent resource.Component + BeforeEach(func() { + validApplicationContainerComponent = resource.Component{ + ID: "placeholder", + Type: resource.ComponentTypeContainer, + Container: &resource.ContainerComponent{ + Type: resource.ComponentTypeContainer, + OSContentType: resource.ContentTypeUBI, + }, + } + + listing.With.Components = []*resource.Component{&validApplicationContainerComponent} + }) + When("the mapping does not contain a corresponding entry", func() { + It("should skip the component and produce an empty host map", func() { + hostmap, err := generateContainerComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(0)) + }) + }) + + When("the mapping contains a corresponding entry", func() { + var validContainerCertTarget ContainerCertTarget + BeforeEach(func() { + validContainerCertTarget = ContainerCertTarget{ + ImageRef: "example.com/example/image", + Tags: []ContainerTagAndOptions{{Tag: "tag"}}, + } + + certTarget[validApplicationContainerComponent.ID] = validContainerCertTarget + }) + + It("should add the container to the hostmap", func() { + hostmap, err := generateContainerComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(1)) + imageTarget := strings.Join([]string{validContainerCertTarget.ImageRef, validContainerCertTarget.Tags[0].Tag}, ":") + hostname := normalizeContainerHostname( + strings.Join([]string{ + validApplicationContainerComponent.ID, + imageTarget, + }, "-"), + ) + Expect(hostmap).To(HaveKey(hostname)) + Expect(hostmap[hostname].Image).To(Equal(imageTarget)) + Expect(hostmap[hostname].Component.ID).To(Equal(validApplicationContainerComponent.ID)) + }) + + When("top level tool flags are defined", func() { + var topLevelToolFlags map[string]any + BeforeEach(func() { + validContainerCertTarget.ToolFlags = map[string]any{} + topLevelToolFlags = map[string]any{"foo": 3} + maps.Copy(validContainerCertTarget.ToolFlags, topLevelToolFlags) + // Re-add the target to the mapping + certTarget[validApplicationContainerComponent.ID] = validContainerCertTarget + }) + + It("should pass the tool flags to the returned host map", func() { + hostmap, err := generateContainerComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(1)) + imageTarget := strings.Join([]string{validContainerCertTarget.ImageRef, validContainerCertTarget.Tags[0].Tag}, ":") + hostname := normalizeContainerHostname( + strings.Join([]string{ + validApplicationContainerComponent.ID, + imageTarget, + }, "-"), + ) + Expect(hostmap[hostname].ToolFlags).To(Equal(topLevelToolFlags)) + }) + + When("tool flags are defined on a specific tag", func() { + var tagSpecificToolFlags map[string]any + BeforeEach(func() { + validContainerCertTarget.Tags[0].ToolFlags = map[string]any{} + tagSpecificToolFlags = map[string]any{"foo": 4} + maps.Copy(validContainerCertTarget.Tags[0].ToolFlags, tagSpecificToolFlags) + // Re-add the target to the mapping + certTarget[validApplicationContainerComponent.ID] = validContainerCertTarget + }) + It("should override top level flags for the specified tag", func() { + hostmap, err := generateContainerComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(1)) + imageTarget := strings.Join([]string{validContainerCertTarget.ImageRef, validContainerCertTarget.Tags[0].Tag}, ":") + hostname := normalizeContainerHostname( + strings.Join([]string{ + validApplicationContainerComponent.ID, + imageTarget, + }, "-"), + ) + Expect(hostmap[hostname].ToolFlags).To(Equal(tagSpecificToolFlags)) + }) + }) + }) + }) + }) + }) + + When("generating inventory for operator components", func() { + var certTarget ComponentCertificationConfig[OperatorCertTarget] + + BeforeEach(func() { + certTarget = ComponentCertificationConfig[OperatorCertTarget]{} + }) + + When("the product contains no operator components", func() { + It("should produce an empty host map", func() { + hostmap, err := generateOperatorComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(0)) + }) + }) + + When("the product contains a valid operator component", func() { + var validOperatorComponent resource.Component + BeforeEach(func() { + validOperatorComponent = resource.Component{ + ID: "placeholder", + Type: resource.ComponentTypeContainer, + Container: &resource.ContainerComponent{ + Type: resource.ComponentTypeContainer, + OSContentType: resource.ContentTypeOperatorBundle, + DistributionMethod: resource.ContainerDistributionExternal, + }, + } + + listing.With.Components = []*resource.Component{&validOperatorComponent} + }) + + When("the mapping does not contain a corresponding entry", func() { + It("should skip the component and produce an empty host map", func() { + hostmap, err := generateOperatorComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(0)) + }) + }) + + When("the mapping contains a corresponding entry", func() { + var ( + validCertTarget OperatorCertTarget + imageTarget string + hostname string + ) + BeforeEach(func() { + validCertTarget = OperatorCertTarget{ + ImageRef: "example.com/example/image", + Tags: []OperatorTagandOptions{{Tag: "tag"}}, + } + + certTarget[validOperatorComponent.ID] = validCertTarget + }) + + JustBeforeEach(func() { + imageTarget = strings.Join([]string{validCertTarget.ImageRef, validCertTarget.Tags[0].Tag}, ":") + hostname = normalizeContainerHostname( + strings.Join([]string{ + validOperatorComponent.ID, + imageTarget, + }, "-"), + ) + }) + + It("should add the operator to the hostmap", func() { + hostmap, err := generateOperatorComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(1)) + Expect(hostmap).To(HaveKey(hostname)) + Expect(hostmap[hostname].Image).To(Equal(imageTarget)) + Expect(hostmap[hostname].Component.ID).To(Equal(validOperatorComponent.ID)) + }) + + When("a top level index image is defined", func() { + var topLevelIndexImage string + BeforeEach(func() { + topLevelIndexImage = "example.io/example/index-image:tag" + validCertTarget.IndexImage = topLevelIndexImage + certTarget[validOperatorComponent.ID] = validCertTarget + }) + + It("should pass the index image to entry", func() { + hostmap, err := generateOperatorComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(1)) + Expect(hostmap[hostname].IndexImage).To(Equal(topLevelIndexImage)) + }) + + When("a tag-specific index image is defined", func() { + var tagSpecificIndexImage string + BeforeEach(func() { + tagSpecificIndexImage = "example.io/example/index-image:tag-specific" + validCertTarget.IndexImage = tagSpecificIndexImage + certTarget[validOperatorComponent.ID] = validCertTarget + }) + + It("should pass the index image to entry", func() { + hostmap, err := generateOperatorComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(1)) + Expect(hostmap[hostname].IndexImage).To(Equal(tagSpecificIndexImage)) + }) + }) + }) + + When("top level tool flags are defined", func() { + var topLevelToolFlags map[string]any + BeforeEach(func() { + validCertTarget.ToolFlags = map[string]any{} + topLevelToolFlags = map[string]any{"foo": 3} + maps.Copy(validCertTarget.ToolFlags, topLevelToolFlags) + // Re-add the target to the mapping + certTarget[validOperatorComponent.ID] = validCertTarget + }) + + It("should pass the tool flags to the returned host map", func() { + hostmap, err := generateOperatorComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(1)) + imageTarget := strings.Join([]string{validCertTarget.ImageRef, validCertTarget.Tags[0].Tag}, ":") + hostname := normalizeContainerHostname( + strings.Join([]string{ + validOperatorComponent.ID, + imageTarget, + }, "-"), + ) + Expect(hostmap[hostname].ToolFlags).To(Equal(topLevelToolFlags)) + }) + + When("tool flags are defined on a specific tag", func() { + var tagSpecificToolFlags map[string]any + BeforeEach(func() { + validCertTarget.Tags[0].ToolFlags = map[string]any{} + tagSpecificToolFlags = map[string]any{"foo": 4} + maps.Copy(validCertTarget.Tags[0].ToolFlags, tagSpecificToolFlags) + // Re-add the target to the mapping + certTarget[validOperatorComponent.ID] = validCertTarget + }) + It("should override top level flags for the specified tag", func() { + hostmap, err := generateOperatorComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(1)) + imageTarget := strings.Join([]string{validCertTarget.ImageRef, validCertTarget.Tags[0].Tag}, ":") + hostname := normalizeContainerHostname( + strings.Join([]string{ + validOperatorComponent.ID, + imageTarget, + }, "-"), + ) + Expect(hostmap[hostname].ToolFlags).To(Equal(tagSpecificToolFlags)) + }) + }) + }) + }) + }) + }) + + When("generating inventory for helm chart components", func() { + var certTarget ComponentCertificationConfig[HelmCertTarget] + + BeforeEach(func() { + certTarget = ComponentCertificationConfig[HelmCertTarget]{} + }) + + When("the product contains no helm chart components", func() { + It("should produce an empty host map", func() { + hostmap, err := generateHelmComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(0)) + }) + }) + + When("the product contains a valid helm chart component", func() { + var validComponent resource.Component + BeforeEach(func() { + validComponent = resource.Component{ + ID: "placeholder", + Type: resource.ComponentTypeHelmChart, + HelmChart: &resource.HelmChartComponent{ + ChartName: "placeholder-chart", + }, + } + + listing.With.Components = []*resource.Component{&validComponent} + }) + + When("the mapping does not contain a corresponding entry", func() { + It("should skip the component and produce an empty host map", func() { + hostmap, err := generateHelmComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(0)) + }) + }) + + When("the mapping contains a corresponding entry", func() { + var ( + validCertTarget HelmCertTarget + chartURI string + hostname string + ) + BeforeEach(func() { + chartURI = "https://example.com/path/to/chart.0.0.1.tgz" + validCertTarget = HelmCertTarget{ + ChartURI: chartURI, + } + + certTarget[validComponent.ID] = validCertTarget + }) + + JustBeforeEach(func() { + hostname = normalizeChartHostname( + strings.Join([]string{ + validComponent.ID, + chartURI, + }, "-"), + ) + fmt.Println(hostname) + }) + + It("should add the helm chart to the hostmap", func() { + hostmap, err := generateHelmComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(1)) + Expect(hostmap).To(HaveKey(hostname)) + Expect(hostmap[hostname].ChartURI).To(Equal(chartURI)) + Expect(hostmap[hostname].Component.ID).To(Equal(validComponent.ID)) + }) + + When("top level tool flags are defined", func() { + var toolFlags map[string]any + BeforeEach(func() { + validCertTarget.ToolFlags = map[string]any{} + toolFlags = map[string]any{"foo": 3} + maps.Copy(validCertTarget.ToolFlags, toolFlags) + // Re-add the target to the mapping + certTarget[validComponent.ID] = validCertTarget + }) + + It("should pass the tool flags to the returned host map", func() { + hostmap, err := generateHelmComponentInventory(ctx, listing, certTarget) + Expect(err).ToNot(HaveOccurred()) + Expect(hostmap).To(HaveLen(1)) + Expect(hostmap[hostname].ToolFlags).To(Equal(toolFlags)) + }) + }) + }) + }) + }) +}) diff --git a/internal/cmd/productctl/cmd/certifycontainers/certifycontainers.go b/internal/cmd/productctl/cmd/certifycontainers/certifycontainers.go index dc262d7..e65b5b1 100644 --- a/internal/cmd/productctl/cmd/certifycontainers/certifycontainers.go +++ b/internal/cmd/productctl/cmd/certifycontainers/certifycontainers.go @@ -74,7 +74,7 @@ func runE(cmd *cobra.Command, args []string) error { } L.Debug("Generating inventory from product and mappings") - inventoryData, err := ansible.GenerateInventory( + inventory, err := ansible.GenerateInventory( cmd.Context(), declaration, mapping, @@ -105,6 +105,11 @@ func runE(cmd *cobra.Command, args []string) error { return err } + inventoryData, err := yaml.Marshal(inventory) + if err != nil { + return err + } + L.Debug("Writing generated inventory to temporary directory", "tmpdir", runBaseDir) err = os.WriteFile(filepath.Join(inventoryDir, "generated.product.inventory.yaml"), inventoryData, 0o600) if err != nil { diff --git a/internal/cmd/productctl/cmd/certifyhelmcharts/certifyhelmcharts.go b/internal/cmd/productctl/cmd/certifyhelmcharts/certifyhelmcharts.go index aa7fd6c..98f57c1 100644 --- a/internal/cmd/productctl/cmd/certifyhelmcharts/certifyhelmcharts.go +++ b/internal/cmd/productctl/cmd/certifyhelmcharts/certifyhelmcharts.go @@ -72,7 +72,7 @@ func runE(cmd *cobra.Command, args []string) error { } L.Debug("Generating inventory from product and mappings") - inventoryData, err := ansible.GenerateInventory( + inventory, err := ansible.GenerateInventory( cmd.Context(), declaration, mapping, @@ -103,6 +103,11 @@ func runE(cmd *cobra.Command, args []string) error { return err } + inventoryData, err := yaml.Marshal(inventory) + if err != nil { + return err + } + L.Debug("Writing generated inventory to temporary directory", "tmpdir", runBaseDir) err = os.WriteFile(filepath.Join(inventoryDir, "generated.product.inventory.yaml"), inventoryData, 0o600) if err != nil { diff --git a/internal/cmd/productctl/cmd/certifyoperators/certifyoperators.go b/internal/cmd/productctl/cmd/certifyoperators/certifyoperators.go index e3ebb43..f12c956 100644 --- a/internal/cmd/productctl/cmd/certifyoperators/certifyoperators.go +++ b/internal/cmd/productctl/cmd/certifyoperators/certifyoperators.go @@ -72,7 +72,7 @@ func runE(cmd *cobra.Command, args []string) error { } L.Debug("Generating inventory from product and mappings") - inventoryData, err := ansible.GenerateInventory( + inventory, err := ansible.GenerateInventory( cmd.Context(), declaration, mapping, @@ -103,6 +103,11 @@ func runE(cmd *cobra.Command, args []string) error { return err } + inventoryData, err := yaml.Marshal(inventory) + if err != nil { + return err + } + L.Debug("Writing generated inventory to temporary directory", "tmpdir", runBaseDir) err = os.WriteFile(filepath.Join(inventoryDir, "generated.product.inventory.yaml"), inventoryData, 0o600) if err != nil {