Skip to content

Commit 88a1c9d

Browse files
committed
Add generate bundle --ignore-if-only-createdAt option
Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com>
1 parent af14062 commit 88a1c9d

File tree

7 files changed

+171
-4
lines changed

7 files changed

+171
-4
lines changed

changelog/fragments/6419.yaml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# entries is a list of entries to include in
2+
# release notes and/or the migration guide
3+
entries:
4+
- description: >
5+
Add ability to ignore bundle updates on `generate bundle` command if createdAt timestamp is the only change.
6+
The flag to use is `--ignore-if-only-createdAt`.
7+
8+
# kind is one of:
9+
# - addition
10+
# - change
11+
# - deprecation
12+
# - removal
13+
# - bugfix
14+
kind: "addition"
15+
16+
# Is this a breaking change?
17+
breaking: false
18+
19+
# NOTE: ONLY USE `pull_request_override` WHEN ADDING THIS
20+
# FILE FOR A PREVIOUSLY MERGED PULL_REQUEST!
21+
#
22+
# The generator auto-detects the PR number from the commit
23+
# message in which this file was originally added.
24+
#
25+
# What is the pull request number (without the "#")?
26+
# pull_request_override: 0
27+
28+
29+
# Migration can be defined to automatically add a section to
30+
# the migration guide. This is required for breaking changes.
31+
# migration:
32+
# header: Header text for the migration section
33+
# body: |
34+
# Body of the migration section. This should be formatted as markdown and can
35+
# span multiple lines.
36+
37+
# Using the YAML string '|' operator means that newlines in this string will
38+
# be honored and interpretted as newlines in the rendered markdown.

internal/cmd/operator-sdk/generate/bundle/bundle.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ func (c bundleCmd) runManifests() (err error) {
201201
opts = append(opts, gencsv.WithWriter(stdout))
202202
} else {
203203
opts = append(opts, gencsv.WithBundleWriter(c.outputDir))
204+
if c.ignoreIfOnlyCreatedAt && genutil.IsExist(c.outputDir) {
205+
opts = append(opts, gencsv.WithBundleReader(c.outputDir))
206+
opts = append(opts, gencsv.WithIgnoreIfOnlyCreatedAt())
207+
}
204208
}
205209

206210
csvGen := gencsv.Generator{

internal/cmd/operator-sdk/generate/bundle/cmd.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ type bundleCmd struct {
4242
extraServiceAccounts []string
4343

4444
// Metadata options.
45-
channels string
46-
defaultChannel string
47-
overwrite bool
45+
channels string
46+
defaultChannel string
47+
overwrite bool
48+
ignoreIfOnlyCreatedAt bool
4849

4950
// These are set if a PROJECT config is not present.
5051
layout string
@@ -138,6 +139,7 @@ func (c *bundleCmd) addFlagsTo(fs *pflag.FlagSet) {
138139
"Names of service accounts, outside of the operator's Deployment account, "+
139140
"that have bindings to {Cluster}Roles that should be added to the CSV")
140141
fs.BoolVar(&c.overwrite, "overwrite", true, "Overwrite the bundle's metadata and Dockerfile if they exist")
142+
fs.BoolVar(&c.ignoreIfOnlyCreatedAt, "ignore-if-only-createdAt", false, "Ignore if only createdAt is changed")
141143
fs.BoolVarP(&c.quiet, "quiet", "q", false, "Run in quiet mode")
142144
fs.BoolVar(&c.stdout, "stdout", false, "Write bundle manifest to stdout")
143145

internal/generate/clusterserviceversion/clusterserviceversion.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"fmt"
1919
"io"
2020
"path/filepath"
21+
"reflect"
2122
"strings"
2223

2324
"github.com/blang/semver/v4"
@@ -61,6 +62,10 @@ type Generator struct {
6162

6263
// Func that returns the writer the generated CSV's bytes are written to.
6364
getWriter func() (io.Writer, error)
65+
// Func that returns the reader the previous CSV's bytes are read from.
66+
getReader func() (io.Reader, error)
67+
68+
ignoreIfOnlyCreatedAt bool
6469
}
6570

6671
// Option is a function that modifies a Generator.
@@ -88,6 +93,22 @@ func WithBundleWriter(dir string) Option {
8893
}
8994
}
9095

96+
// WithBundleGetter sets a Generator's getter to a bundle CSV file under
97+
// <dir>/manifests.
98+
func WithBundleReader(dir string) Option {
99+
return func(g *Generator) error {
100+
fileName := makeCSVFileName(g.OperatorName)
101+
g.getReader = func() (io.Reader, error) {
102+
return bundleReader(dir, fileName)
103+
}
104+
return nil
105+
}
106+
}
107+
108+
func bundleReader(dir, fileName string) (io.Reader, error) {
109+
return genutil.Open(filepath.Join(dir, bundle.ManifestsDir), fileName)
110+
}
111+
91112
// WithPackageWriter sets a Generator's writer to a package CSV file under
92113
// <dir>/<version>.
93114
func WithPackageWriter(dir string) Option {
@@ -100,6 +121,13 @@ func WithPackageWriter(dir string) Option {
100121
}
101122
}
102123

124+
func WithIgnoreIfOnlyCreatedAt() Option {
125+
return func(g *Generator) error {
126+
g.ignoreIfOnlyCreatedAt = true
127+
return nil
128+
}
129+
}
130+
103131
// Generate configures the generator with col and opts then runs it.
104132
func (g *Generator) Generate(opts ...Option) (err error) {
105133
for _, opt := range opts {
@@ -119,7 +147,33 @@ func (g *Generator) Generate(opts ...Option) (err error) {
119147

120148
// Add extra annotations to csv
121149
g.setAnnotations(csv)
122-
150+
// If a reader is set, and there is a flag to not update createdAt, then
151+
// set the CSV's createdAt to the previous CSV's createdAt if its the only change.
152+
if g.ignoreIfOnlyCreatedAt && g.getReader != nil {
153+
r, err := g.getReader()
154+
if err != nil {
155+
return err
156+
}
157+
var prevCSV operatorsv1alpha1.ClusterServiceVersion
158+
err = genutil.ReadObject(r, &prevCSV)
159+
if err != nil {
160+
return err
161+
}
162+
if prevCSV.ObjectMeta.Annotations != nil && prevCSV.ObjectMeta.Annotations["createdAt"] != "" {
163+
csvWithoutCreatedAtChange := csv.DeepCopy()
164+
// Set WebhookDefinitions if nil to avoid diffing on it
165+
if prevCSV.Spec.WebhookDefinitions == nil {
166+
prevCSV.Spec.WebhookDefinitions = []operatorsv1alpha1.WebhookDescription{}
167+
}
168+
if csvWithoutCreatedAtChange.ObjectMeta.Annotations == nil {
169+
csvWithoutCreatedAtChange.ObjectMeta.Annotations = map[string]string{}
170+
}
171+
csvWithoutCreatedAtChange.ObjectMeta.Annotations["createdAt"] = prevCSV.ObjectMeta.Annotations["createdAt"]
172+
if reflect.DeepEqual(csvWithoutCreatedAtChange, &prevCSV) {
173+
csv = csvWithoutCreatedAtChange
174+
}
175+
}
176+
}
123177
w, err := g.getWriter()
124178
if err != nil {
125179
return err

internal/generate/clusterserviceversion/clusterserviceversion_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/onsi/gomega/format"
2828
operatorversion "github.com/operator-framework/api/pkg/lib/version"
2929
"github.com/operator-framework/api/pkg/operators/v1alpha1"
30+
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
3031
"github.com/operator-framework/operator-registry/pkg/lib/bundle"
3132
appsv1 "k8s.io/api/apps/v1"
3233
"sigs.k8s.io/yaml"
@@ -140,6 +141,56 @@ var _ = Describe("Testing CRDs with single version", func() {
140141
Expect(outputFile).To(BeAnExistingFile())
141142
Expect(readFileHelper(outputFile)).To(MatchYAML(newCSVUIMetaStr))
142143
})
144+
It("should not update createdAt to ClusterServiceVersion manifest to a bundle file if it's the only change", func() {
145+
g = Generator{
146+
OperatorName: operatorName,
147+
Version: zeroZeroOne,
148+
Collector: col,
149+
}
150+
opts := []Option{
151+
WithBundleWriter(tmp),
152+
}
153+
Expect(g.Generate(opts...)).ToNot(HaveOccurred())
154+
outputFile := filepath.Join(tmp, bundle.ManifestsDir, makeCSVFileName(operatorName))
155+
Expect(outputFile).To(BeAnExistingFile())
156+
Expect(readFileHelper(outputFile)).To(MatchYAML(newCSVUIMetaStr))
157+
var initiallyWrittenCSV operatorsv1alpha1.ClusterServiceVersion
158+
r, err := bundleReader(tmp, makeCSVFileName(operatorName))
159+
Expect(err).ToNot(HaveOccurred())
160+
err = genutil.ReadObject(r, &initiallyWrittenCSV)
161+
Expect(err).ToNot(HaveOccurred())
162+
Expect(initiallyWrittenCSV.ObjectMeta.Annotations).ToNot(BeNil())
163+
Expect(initiallyWrittenCSV.ObjectMeta.Annotations["createdAt"]).ToNot(Equal(""))
164+
g = Generator{
165+
OperatorName: operatorName,
166+
Version: zeroZeroOne,
167+
Collector: col,
168+
}
169+
opts = []Option{
170+
WithBundleWriter(tmp),
171+
WithBundleReader(tmp),
172+
WithIgnoreIfOnlyCreatedAt(),
173+
}
174+
time.Sleep(1*time.Second + 1*time.Millisecond) // sleep to ensure createdAt is different if not for ignore option
175+
Expect(g.Generate(opts...)).ToNot(HaveOccurred())
176+
Expect(outputFile).To(BeAnExistingFile())
177+
// This should fail if createdAt changed.
178+
Expect(readFileHelper(outputFile)).To(MatchYAML(newCSVUIMetaStr))
179+
// now try without ignore option
180+
g = Generator{
181+
OperatorName: operatorName,
182+
Version: zeroZeroOne,
183+
Collector: col,
184+
}
185+
opts = []Option{
186+
WithBundleWriter(tmp),
187+
WithBundleReader(tmp),
188+
}
189+
Expect(g.Generate(opts...)).ToNot(HaveOccurred())
190+
Expect(outputFile).To(BeAnExistingFile())
191+
// This should fail if createdAt changed.
192+
Expect(readFileHelper(outputFile)).ToNot(MatchYAML(newCSVUIMetaStr))
193+
})
143194
It("should write a ClusterServiceVersion manifest to a package file", func() {
144195
g = Generator{
145196
OperatorName: operatorName,

internal/generate/internal/genutil.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"os"
2323
"path/filepath"
2424

25+
"sigs.k8s.io/controller-runtime/pkg/client"
2526
"sigs.k8s.io/yaml"
2627

2728
"github.com/operator-framework/operator-sdk/internal/util/k8sutil"
@@ -111,3 +112,11 @@ func IsNotExist(path string) bool {
111112
_, err := os.Stat(path)
112113
return err != nil && errors.Is(err, os.ErrNotExist)
113114
}
115+
116+
func ReadObject(r io.Reader, obj client.Object) error {
117+
var buf bytes.Buffer
118+
if _, err := buf.ReadFrom(r); err != nil {
119+
return err
120+
}
121+
return k8sutil.GetObjectFromBytes(buf.Bytes(), obj)
122+
}

internal/util/k8sutil/object.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package k8sutil
1616

1717
import (
1818
"k8s.io/apimachinery/pkg/runtime"
19+
"sigs.k8s.io/yaml"
1920
)
2021

2122
type MarshalFunc func(interface{}) ([]byte, error)
@@ -53,3 +54,11 @@ func deleteKeyFromUnstructured(u map[string]interface{}, key string) {
5354
}
5455
}
5556
}
57+
58+
func GetObjectFromBytes(b []byte, obj interface{}) error {
59+
var u map[string]interface{}
60+
if err := yaml.Unmarshal(b, &u); err != nil {
61+
return err
62+
}
63+
return runtime.DefaultUnstructuredConverter.FromUnstructured(u, obj)
64+
}

0 commit comments

Comments
 (0)