diff --git a/bundle/manifests/oadp-operator.clusterserviceversion.yaml b/bundle/manifests/oadp-operator.clusterserviceversion.yaml index 44481ede449..a390c833ad3 100644 --- a/bundle/manifests/oadp-operator.clusterserviceversion.yaml +++ b/bundle/manifests/oadp-operator.clusterserviceversion.yaml @@ -1461,9 +1461,9 @@ spec: - name: RELATED_IMAGE_KUBEVIRT_VELERO_PLUGIN value: quay.io/konveyor/kubevirt-velero-plugin:latest - name: RELATED_IMAGE_HYPERSHIFT_VELERO_PLUGIN - value: quay.io/redhat-user-workloads/ocp-art-tenant/oadp-hypershift-oadp-plugin-main:main + value: quay.io/konveyor/hypershift-oadp-plugin:latest - name: RELATED_IMAGE_MUSTGATHER - value: registry.redhat.io/oadp/oadp-mustgather-rhel8:v1.2 + value: quay.io/konveyor/oadp-must-gather:latest - name: RELATED_IMAGE_NON_ADMIN_CONTROLLER value: quay.io/konveyor/oadp-non-admin:latest - name: RELATED_IMAGE_VM_FILE_RESTORE_CONTROLLER @@ -1480,6 +1480,8 @@ spec: value: quay.io/konveyor/oadp-vmdp-binaries:latest - name: RELATED_IMAGE_KUBEVIRT_DATAMOVER_CONTROLLER value: quay.io/konveyor/kubevirt-datamover-controller:latest + - name: RELATED_IMAGE_KUBEVIRT_DATAMOVER_PLUGIN + value: quay.io/konveyor/kubevirt-datamover-plugin:latest image: quay.io/konveyor/oadp-operator:latest imagePullPolicy: Always livenessProbe: @@ -1702,9 +1704,9 @@ spec: name: velero-plugin-for-gcp - image: quay.io/konveyor/kubevirt-velero-plugin:latest name: kubevirt-velero-plugin - - image: quay.io/redhat-user-workloads/ocp-art-tenant/oadp-hypershift-oadp-plugin-main:main + - image: quay.io/konveyor/hypershift-oadp-plugin:latest name: hypershift-velero-plugin - - image: registry.redhat.io/oadp/oadp-mustgather-rhel8:v1.2 + - image: quay.io/konveyor/oadp-must-gather:latest name: mustgather - image: quay.io/konveyor/oadp-non-admin:latest name: non-admin-controller @@ -1722,4 +1724,6 @@ spec: name: vmdp-cli-download - image: quay.io/konveyor/kubevirt-datamover-controller:latest name: kubevirt-datamover-controller + - image: quay.io/konveyor/kubevirt-datamover-plugin:latest + name: kubevirt-datamover-plugin version: 99.0.0 diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index fe9383b4b8b..29940d72ecf 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -83,9 +83,9 @@ spec: - name: RELATED_IMAGE_KUBEVIRT_VELERO_PLUGIN value: quay.io/konveyor/kubevirt-velero-plugin:latest - name: RELATED_IMAGE_HYPERSHIFT_VELERO_PLUGIN - value: quay.io/redhat-user-workloads/ocp-art-tenant/oadp-hypershift-oadp-plugin-main:main + value: quay.io/konveyor/hypershift-oadp-plugin:latest - name: RELATED_IMAGE_MUSTGATHER - value: registry.redhat.io/oadp/oadp-mustgather-rhel8:v1.2 + value: quay.io/konveyor/oadp-must-gather:latest - name: RELATED_IMAGE_NON_ADMIN_CONTROLLER value: quay.io/konveyor/oadp-non-admin:latest - name: RELATED_IMAGE_VM_FILE_RESTORE_CONTROLLER @@ -102,6 +102,8 @@ spec: value: quay.io/konveyor/oadp-vmdp-binaries:latest - name: RELATED_IMAGE_KUBEVIRT_DATAMOVER_CONTROLLER value: quay.io/konveyor/kubevirt-datamover-controller:latest + - name: RELATED_IMAGE_KUBEVIRT_DATAMOVER_PLUGIN + value: quay.io/konveyor/kubevirt-datamover-plugin:latest args: - --leader-elect image: controller:latest diff --git a/go.mod b/go.mod index 757205b5cc5..9dc0887c640 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,7 @@ require ( github.com/vmware-tanzu/velero v1.14.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 google.golang.org/api v0.256.0 + gopkg.in/yaml.v3 v3.0.1 k8s.io/klog/v2 v2.130.1 ) @@ -193,7 +194,6 @@ require ( google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/cli-runtime v0.33.3 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect diff --git a/must-gather/pkg/templates/summary.go b/must-gather/pkg/templates/summary.go index dbd921c6126..42aefb60644 100644 --- a/must-gather/pkg/templates/summary.go +++ b/must-gather/pkg/templates/summary.go @@ -251,7 +251,6 @@ func ReplaceClusterInformationSection( summaryTemplateReplaces["OCP_VERSION"] = clusterVersion.Status.Desired.Version summaryTemplateReplaces["CLUSTER_VERSION"] = createYAML(outputPath, "cluster-scoped-resources/config.openshift.io/clusterversions.yaml", clusterVersion) cloudProvider := "" - if infrastructureList != nil && len(infrastructureList.Items) != 0 { cloudProvider = string(infrastructureList.Items[0].Spec.PlatformSpec.Type) @@ -480,7 +479,6 @@ func humanizeDurationSince(t time.Time) string { return time.Since(t).Round(time.Second).String() } - func ReplaceCloudStoragesSection(outputPath string, cloudStorageList *oadpv1alpha1.CloudStorageList) { if cloudStorageList != nil && len(cloudStorageList.Items) != 0 { cloudStorageByNamespace := map[string][]oadpv1alpha1.CloudStorage{} @@ -1668,7 +1666,7 @@ func ReplaceCustomResourceDefinitionsSection(outputPath string, clusterConfig *r // CRD spec.names.plural : CRD spec.group crds := map[string]string{ "dataprotectionapplications": gvk.DataProtectionApplicationGVK.Group, - "dataprotectiontests": gvk.DataProtectionTestGVK.Group, + "dataprotectiontests": gvk.DataProtectionTestGVK.Group, "cloudstorages": gvk.CloudStorageGVK.Group, "backupstoragelocations": gvk.BackupStorageLocationGVK.Group, "volumesnapshotlocations": gvk.VolumeSnapshotLocationGVK.Group, diff --git a/tests/release/helpers_test.go b/tests/release/helpers_test.go new file mode 100644 index 00000000000..27d06800f2b --- /dev/null +++ b/tests/release/helpers_test.go @@ -0,0 +1,74 @@ +package release + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func repoRoot(t *testing.T) string { + t.Helper() + _, filename, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("unable to determine test file path") + } + return filepath.Dir(filepath.Dir(filepath.Dir(filename))) +} + +// branchVersion returns the oadp-X.Y version from the current branch. +// Checks PULL_BASE_REF first (set by Prow to the PR target branch), +// then falls back to the local git branch name. +func branchVersion(t *testing.T) string { + t.Helper() + if ref := os.Getenv("PULL_BASE_REF"); strings.HasPrefix(ref, oadpBranchPrefix) { + return ref + } + out, err := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output() + if err != nil { + t.Fatalf("failed to get current branch: %v", err) + } + branch := strings.TrimSpace(string(out)) + if !strings.HasPrefix(branch, oadpBranchPrefix) { + return "" + } + return branch +} + +func readBundleFile(t *testing.T, root, relPath string) []byte { + t.Helper() + path := filepath.Join(root, relPath) + data, err := os.ReadFile(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + t.Skipf("%s does not exist (only present on release branches)", relPath) + } + t.Fatalf("failed to read %s: %v", relPath, err) + } + return data +} + +func reportErrors(t *testing.T, errs []error) { + t.Helper() + for _, err := range errs { + t.Error(err) + } +} + +func assertErrors(t *testing.T, errs []error, wantErrs int, wantMsg string) { + t.Helper() + if len(errs) != wantErrs { + t.Fatalf("got %d errors, want %d: %v", len(errs), wantErrs, errs) + } + if wantMsg != "" { + for _, err := range errs { + if strings.Contains(err.Error(), wantMsg) { + return + } + } + t.Errorf("expected error containing %q, got %v", wantMsg, errs) + } +} diff --git a/tests/release/image_references.go b/tests/release/image_references.go new file mode 100644 index 00000000000..fe9b9d0c6b2 --- /dev/null +++ b/tests/release/image_references.go @@ -0,0 +1,136 @@ +package release + +import ( + "fmt" + "strings" + + "gopkg.in/yaml.v3" +) + +func parseImageReferences(data []byte) (*imageReferencesFile, error) { + var ir imageReferencesFile + if err := yaml.Unmarshal(data, &ir); err != nil { + return nil, fmt.Errorf("failed to parse image-references: %w", err) + } + if len(ir.Spec.Tags) == 0 { + return nil, fmt.Errorf("image-references has no tags") + } + return &ir, nil +} + +func parseCSV(data []byte) (*csv, error) { + var c csv + if err := yaml.Unmarshal(data, &c); err != nil { + return nil, fmt.Errorf("failed to parse CSV: %w", err) + } + return &c, nil +} + +// csvImages extracts RELATED_IMAGE_* env var values and container images +// from the CSV's deployment specs. +func csvImages(c *csv) (relatedImages, containerImages map[string]bool) { + relatedImages = make(map[string]bool) + containerImages = make(map[string]bool) + for _, dep := range c.Spec.Install.Spec.Deployments { + for _, container := range dep.Spec.Template.Spec.Containers { + containerImages[container.Image] = true + for _, env := range container.Env { + if strings.HasPrefix(env.Name, relatedImagePrefix) { + relatedImages[env.Value] = true + } + } + } + } + return +} + +// irImages extracts the set of image references from image-references tags. +func irImages(ir *imageReferencesFile) map[string]bool { + images := make(map[string]bool, len(ir.Spec.Tags)) + for _, tag := range ir.Spec.Tags { + images[tag.From.Name] = true + } + return images +} + +// ValidateImageReferencesTagVersion checks that every image tag uses the +// expected release version (e.g. ":oadp-1.6"), preventing accidental use +// of tags from a different release stream. +func ValidateImageReferencesTagVersion(imageRefsData []byte, expectedVersion string, exceptions []string) []error { + ir, err := parseImageReferences(imageRefsData) + if err != nil { + return []error{err} + } + + skip := make(map[string]bool, len(exceptions)) + for _, name := range exceptions { + skip[name] = true + } + + expectedSuffix := ":" + expectedVersion + var errs []error + for _, tag := range ir.Spec.Tags { + if skip[tag.Name] { + continue + } + if !strings.HasSuffix(tag.From.Name, expectedSuffix) { + errs = append(errs, fmt.Errorf("image-references tag %q has image %q which does not end with %q", tag.Name, tag.From.Name, expectedSuffix)) + } + } + return errs +} + +// ValidateImageReferencesMatchCSV ensures every image in image-references has +// a corresponding RELATED_IMAGE_* env var (or is a container image) in the CSV. +// This catches images added to image-references but not wired into the operator. +func ValidateImageReferencesMatchCSV(imageRefsData, csvData []byte) []error { + ir, err := parseImageReferences(imageRefsData) + if err != nil { + return []error{err} + } + + c, err := parseCSV(csvData) + if err != nil { + return []error{err} + } + + relatedImages, containerImages := csvImages(c) + + var errs []error + for _, tag := range ir.Spec.Tags { + dockerImage := tag.From.Name + if containerImages[dockerImage] { + continue + } + if !relatedImages[dockerImage] { + errs = append(errs, fmt.Errorf("image-references tag %q has image %q which is not in CSV RELATED_IMAGE_* env vars", tag.Name, dockerImage)) + } + } + return errs +} + +// ValidateCSVMatchImageReferences ensures every RELATED_IMAGE_* env var in the +// CSV has a corresponding entry in image-references. This catches orphaned env +// vars that reference images no longer tracked in image-references. +func ValidateCSVMatchImageReferences(imageRefsData, csvData []byte) []error { + ir, err := parseImageReferences(imageRefsData) + if err != nil { + return []error{err} + } + + c, err := parseCSV(csvData) + if err != nil { + return []error{err} + } + + known := irImages(ir) + relatedImages, _ := csvImages(c) + + var errs []error + for image := range relatedImages { + if !known[image] { + errs = append(errs, fmt.Errorf("CSV RELATED_IMAGE_* has image %q which is not in image-references", image)) + } + } + return errs +} diff --git a/tests/release/image_references_test.go b/tests/release/image_references_test.go new file mode 100644 index 00000000000..6d51a26f7b3 --- /dev/null +++ b/tests/release/image_references_test.go @@ -0,0 +1,439 @@ +package release + +import ( + "testing" +) + +func TestImageReferencesTagVersion(t *testing.T) { + version := branchVersion(t) + if version == "" { + t.Skip("not on an oadp-X.Y release branch") + } + + root := repoRoot(t) + irData := readBundleFile(t, root, imageRefsRelPath) + + // Add tag names here for images that use their own versioning scheme + // (e.g. external dependencies not built from this repo). + exceptions := []string{} + + reportErrors(t, ValidateImageReferencesTagVersion(irData, version, exceptions)) +} + +func TestImageReferencesMatchCSV(t *testing.T) { + root := repoRoot(t) + irData := readBundleFile(t, root, imageRefsRelPath) + csvData := readBundleFile(t, root, csvRelPath) + + reportErrors(t, ValidateImageReferencesMatchCSV(irData, csvData)) +} + +func TestCSVMatchImageReferences(t *testing.T) { + root := repoRoot(t) + irData := readBundleFile(t, root, imageRefsRelPath) + csvData := readBundleFile(t, root, csvRelPath) + + reportErrors(t, ValidateCSVMatchImageReferences(irData, csvData)) +} + +func TestValidateImageReferencesMatchCSV(t *testing.T) { + tests := []struct { + name string + imageRefs string + csv string + wantErrs int + wantMsg string + }{ + { + name: "all images present as RELATED_IMAGE", + imageRefs: `spec: + tags: + - name: velero + from: + kind: DockerImage + name: registry.example.com/velero:latest + - name: aws-plugin + from: + kind: DockerImage + name: registry.example.com/aws-plugin:latest`, + csv: `spec: + install: + spec: + deployments: + - spec: + template: + spec: + containers: + - image: registry.example.com/operator:latest + env: + - name: RELATED_IMAGE_VELERO + value: registry.example.com/velero:latest + - name: RELATED_IMAGE_AWS_PLUGIN + value: registry.example.com/aws-plugin:latest`, + wantErrs: 0, + }, + { + name: "missing image produces error", + imageRefs: `spec: + tags: + - name: velero + from: + kind: DockerImage + name: registry.example.com/velero:latest + - name: missing-plugin + from: + kind: DockerImage + name: registry.example.com/missing:latest`, + csv: `spec: + install: + spec: + deployments: + - spec: + template: + spec: + containers: + - image: registry.example.com/operator:latest + env: + - name: RELATED_IMAGE_VELERO + value: registry.example.com/velero:latest`, + wantErrs: 1, + wantMsg: "missing-plugin", + }, + { + name: "operator container image is skipped", + imageRefs: `spec: + tags: + - name: oadp-operator + from: + kind: DockerImage + name: registry.example.com/operator:latest`, + csv: `spec: + install: + spec: + deployments: + - spec: + template: + spec: + containers: + - image: registry.example.com/operator:latest + env: []`, + wantErrs: 0, + }, + { + name: "empty tags produces error", + imageRefs: `spec: { tags: [] }`, + csv: `spec: {}`, + wantErrs: 1, + wantMsg: "no tags", + }, + { + name: "invalid image-references YAML produces error", + imageRefs: `{{{`, + csv: `spec: {}`, + wantErrs: 1, + wantMsg: "failed to parse image-references", + }, + { + name: "invalid CSV YAML produces error", + imageRefs: `spec: + tags: + - name: velero + from: + kind: DockerImage + name: registry.example.com/velero:latest`, + csv: `{{{`, + wantErrs: 1, + wantMsg: "failed to parse CSV", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errs := ValidateImageReferencesMatchCSV([]byte(tt.imageRefs), []byte(tt.csv)) + assertErrors(t, errs, tt.wantErrs, tt.wantMsg) + }) + } +} + +func TestValidateImageReferencesTagVersion(t *testing.T) { + tests := []struct { + name string + imageRefs string + version string + exceptions []string + wantErrs int + wantMsg string + }{ + { + name: "all tags match version", + imageRefs: `spec: + tags: + - name: oadp-rhel9-operator + from: + kind: DockerImage + name: quay.io/konveyor/oadp-operator:oadp-1.6 + - name: oadp-velero-rhel9 + from: + kind: DockerImage + name: quay.io/konveyor/velero:oadp-1.6`, + version: "oadp-1.6", + wantErrs: 0, + }, + { + name: "wrong version tag produces error", + imageRefs: `spec: + tags: + - name: oadp-rhel9-operator + from: + kind: DockerImage + name: quay.io/konveyor/oadp-operator:oadp-1.6 + - name: oadp-velero-rhel9 + from: + kind: DockerImage + name: quay.io/konveyor/velero:oadp-1.5`, + version: "oadp-1.6", + wantErrs: 1, + wantMsg: "oadp-velero-rhel9", + }, + { + name: "non-release tag produces error", + imageRefs: `spec: + tags: + - name: oadp-kubevirt-velero-plugin-rhel9 + from: + kind: DockerImage + name: quay.io/konveyor/kubevirt-velero-plugin:v0.7.0`, + version: "oadp-1.6", + wantErrs: 1, + wantMsg: "v0.7.0", + }, + { + name: "excepted tag is skipped", + imageRefs: `spec: + tags: + - name: oadp-rhel9-operator + from: + kind: DockerImage + name: quay.io/konveyor/oadp-operator:oadp-1.6 + - name: oadp-kubevirt-velero-plugin-rhel9 + from: + kind: DockerImage + name: quay.io/konveyor/kubevirt-velero-plugin:v0.7.0`, + version: "oadp-1.6", + exceptions: []string{"oadp-kubevirt-velero-plugin-rhel9"}, + wantErrs: 0, + }, + { + name: "only non-excepted mismatches produce errors", + imageRefs: `spec: + tags: + - name: oadp-velero-rhel9 + from: + kind: DockerImage + name: quay.io/konveyor/velero:oadp-1.5 + - name: oadp-kubevirt-velero-plugin-rhel9 + from: + kind: DockerImage + name: quay.io/konveyor/kubevirt-velero-plugin:v0.7.0`, + version: "oadp-1.6", + exceptions: []string{"oadp-kubevirt-velero-plugin-rhel9"}, + wantErrs: 1, + wantMsg: "oadp-velero-rhel9", + }, + { + name: "multiple mismatches", + imageRefs: `spec: + tags: + - name: image-a + from: + kind: DockerImage + name: registry.example.com/a:wrong + - name: image-b + from: + kind: DockerImage + name: registry.example.com/b:also-wrong`, + version: "oadp-1.6", + wantErrs: 2, + }, + { + name: "empty tags produces error", + imageRefs: `spec: { tags: [] }`, + version: "oadp-1.6", + wantErrs: 1, + wantMsg: "no tags", + }, + { + name: "invalid YAML produces error", + imageRefs: `{{{`, + version: "oadp-1.6", + wantErrs: 1, + wantMsg: "failed to parse image-references", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errs := ValidateImageReferencesTagVersion([]byte(tt.imageRefs), tt.version, tt.exceptions) + assertErrors(t, errs, tt.wantErrs, tt.wantMsg) + }) + } +} + +func TestValidateCSVMatchImageReferences(t *testing.T) { + tests := []struct { + name string + imageRefs string + csv string + wantErrs int + wantMsg string + }{ + { + name: "all RELATED_IMAGEs have matching image-references entry", + imageRefs: `spec: + tags: + - name: velero + from: + kind: DockerImage + name: registry.example.com/velero:latest`, + csv: `spec: + install: + spec: + deployments: + - spec: + template: + spec: + containers: + - image: registry.example.com/operator:latest + env: + - name: RELATED_IMAGE_VELERO + value: registry.example.com/velero:latest`, + wantErrs: 0, + }, + { + name: "orphaned RELATED_IMAGE produces error", + imageRefs: `spec: + tags: + - name: velero + from: + kind: DockerImage + name: registry.example.com/velero:latest`, + csv: `spec: + install: + spec: + deployments: + - spec: + template: + spec: + containers: + - image: registry.example.com/operator:latest + env: + - name: RELATED_IMAGE_VELERO + value: registry.example.com/velero:latest + - name: RELATED_IMAGE_ORPHAN + value: registry.example.com/orphan:latest`, + wantErrs: 1, + wantMsg: "registry.example.com/orphan:latest", + }, + { + name: "non-RELATED_IMAGE env vars are ignored", + imageRefs: `spec: + tags: + - name: operator + from: + kind: DockerImage + name: registry.example.com/operator:latest`, + csv: `spec: + install: + spec: + deployments: + - spec: + template: + spec: + containers: + - image: registry.example.com/operator:latest + env: + - name: WATCH_NAMESPACE + value: openshift-adp`, + wantErrs: 0, + }, + { + name: "multiple orphaned RELATED_IMAGEs", + imageRefs: `spec: + tags: + - name: unrelated + from: + kind: DockerImage + name: registry.example.com/unrelated:latest`, + csv: `spec: + install: + spec: + deployments: + - spec: + template: + spec: + containers: + - image: registry.example.com/operator:latest + env: + - name: RELATED_IMAGE_A + value: registry.example.com/a:latest + - name: RELATED_IMAGE_B + value: registry.example.com/b:latest`, + wantErrs: 2, + }, + { + name: "empty tags produces error", + imageRefs: `spec: { tags: [] }`, + csv: `spec: + install: + spec: + deployments: + - spec: + template: + spec: + containers: + - image: registry.example.com/operator:latest + env: + - name: RELATED_IMAGE_X + value: registry.example.com/x:latest`, + wantErrs: 1, + wantMsg: "no tags", + }, + { + name: "invalid image-references YAML produces error", + imageRefs: `{{{`, + csv: `spec: + install: + spec: + deployments: + - spec: + template: + spec: + containers: + - image: registry.example.com/operator:latest + env: + - name: RELATED_IMAGE_VELERO + value: registry.example.com/velero:latest`, + wantErrs: 1, + wantMsg: "failed to parse image-references", + }, + { + name: "invalid CSV YAML produces error", + imageRefs: `spec: + tags: + - name: velero + from: + kind: DockerImage + name: registry.example.com/velero:latest`, + csv: `{{{`, + wantErrs: 1, + wantMsg: "failed to parse CSV", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errs := ValidateCSVMatchImageReferences([]byte(tt.imageRefs), []byte(tt.csv)) + assertErrors(t, errs, tt.wantErrs, tt.wantMsg) + }) + } +} diff --git a/tests/release/types.go b/tests/release/types.go new file mode 100644 index 00000000000..b8479f12a1c --- /dev/null +++ b/tests/release/types.go @@ -0,0 +1,45 @@ +package release + +const ( + relatedImagePrefix = "RELATED_IMAGE_" + oadpBranchPrefix = "oadp-" + + imageRefsRelPath = "bundle/image-references" + csvRelPath = "bundle/manifests/oadp-operator.clusterserviceversion.yaml" +) + +type imageReferencesFile struct { + Spec struct { + Tags []struct { + Name string `yaml:"name"` + From struct { + Kind string `yaml:"kind"` + Name string `yaml:"name"` + } `yaml:"from"` + } `yaml:"tags"` + } `yaml:"spec"` +} + +type csv struct { + Spec struct { + Install struct { + Spec struct { + Deployments []struct { + Spec struct { + Template struct { + Spec struct { + Containers []struct { + Image string `yaml:"image"` + Env []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"env"` + } `yaml:"containers"` + } `yaml:"spec"` + } `yaml:"template"` + } `yaml:"spec"` + } `yaml:"deployments"` + } `yaml:"spec"` + } `yaml:"install"` + } `yaml:"spec"` +}