Skip to content

Commit 87e1fc4

Browse files
authoredJun 30, 2020
[sdk-stamps] add sdk stamps to bundle images (#3120)
* [sdk-stamps] add sdk stamps to bundle images Add SDK related metric labels and annotations to bundle images * [sdk-stamps] Add sdk stamps to CSV * [sdk-stamps] Add test case for csv stamps * [sdk-stamps] Add stamps to crds * [sdk-stamps] Modify test case for addition of stamps in crds * [sdk-stamps] modify test scripts * [sdk-stamps] update implementation with operator-registry dependency * [sdk-stamps] addressing review comments * [sdk-stamps] Remove addition of sdk labels from crd Co-Authored by: Eric Stroczynski <[email protected]>
1 parent cb8bf47 commit 87e1fc4

File tree

8 files changed

+341
-9
lines changed

8 files changed

+341
-9
lines changed
 
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
entries:
2+
- description: Add sdk annotations to bundle resources (CSVs, `annotations.yaml` and `bundle.dockerfile`).
3+
kind: "addition"

‎cmd/operator-sdk/bundle/create.go

+56-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
catalog "github.com/operator-framework/operator-sdk/internal/generate/olm-catalog"
2525
"github.com/operator-framework/operator-sdk/internal/util/projutil"
2626

27+
"github.com/operator-framework/operator-sdk/internal/registry"
28+
2729
"github.com/blang/semver"
2830
"github.com/operator-framework/operator-registry/pkg/lib/bundle"
2931
scorecard "github.com/operator-framework/operator-sdk/internal/scorecard/alpha"
@@ -234,7 +236,14 @@ func (c bundleCreateCmd) runGenerate() error {
234236
if err != nil {
235237
return fmt.Errorf("error generating bundle image files: %v", err)
236238
}
237-
if err = copyScorecardConfig(); err != nil {
239+
240+
// rootDir is the location where metadata directory is present.
241+
rootDir := c.outputDir
242+
if rootDir == "" {
243+
rootDir = filepath.Dir(c.directory)
244+
}
245+
246+
if err = RewriteBundleImageContents(rootDir); err != nil {
238247
return err
239248
}
240249
return nil
@@ -330,3 +339,49 @@ func copyScorecardConfig() error {
330339
}
331340
return nil
332341
}
342+
343+
func addLabelsToDockerfile(filename string, metricAnnotation map[string]string) error {
344+
var sdkMetricContent strings.Builder
345+
for key, value := range metricAnnotation {
346+
sdkMetricContent.WriteString(fmt.Sprintf("LABEL %s=%s\n", key, value))
347+
}
348+
349+
err := projutil.RewriteFileContents(filename, "LABEL", sdkMetricContent.String())
350+
if err != nil {
351+
return fmt.Errorf("error rewriting dockerfile with metric labels, %v", err)
352+
}
353+
return nil
354+
}
355+
356+
func RewriteBundleImageContents(rootDir string) error {
357+
metricLabels := projutil.MakeBundleMetricsLabels()
358+
359+
// write metric labels to bundle.Dockerfile
360+
if err := addLabelsToDockerfile(bundle.DockerFile, metricLabels); err != nil {
361+
return fmt.Errorf("error writing metric labels to bundle.dockerfile: %v", err)
362+
}
363+
364+
annotationsFilePath := getAnnotationsFilePath(rootDir)
365+
if err := addLabelsToAnnotations(annotationsFilePath, metricLabels); err != nil {
366+
return fmt.Errorf("error writing metric labels to annotations.yaml: %v", err)
367+
}
368+
369+
// Add a COPY for the scorecard config to bundle.Dockerfile.
370+
if err := copyScorecardConfig(); err != nil {
371+
return fmt.Errorf("error copying scorecardConfig to bundle image, %v", err)
372+
}
373+
return nil
374+
}
375+
376+
// getAnnotationsFilePath return the locations of annotations.yaml.
377+
func getAnnotationsFilePath(rootDir string) string {
378+
return filepath.Join(rootDir, bundle.MetadataDir, bundle.AnnotationsFile)
379+
}
380+
381+
func addLabelsToAnnotations(filename string, metricLables map[string]string) error {
382+
err := registry.RewriteAnnotationsYaml(filename, metricLables)
383+
if err != nil {
384+
return err
385+
}
386+
return nil
387+
}

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

+31
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/operator-framework/operator-registry/pkg/lib/bundle"
2525
"sigs.k8s.io/kubebuilder/pkg/model/config"
2626

27+
genbundle "github.com/operator-framework/operator-sdk/cmd/operator-sdk/bundle"
2728
genutil "github.com/operator-framework/operator-sdk/cmd/operator-sdk/generate/internal"
2829
gencsv "github.com/operator-framework/operator-sdk/internal/generate/clusterserviceversion"
2930
"github.com/operator-framework/operator-sdk/internal/generate/collector"
@@ -255,9 +256,39 @@ func (c bundleCmd) runMetadata() error {
255256

256257
// generateMetadata wraps the operator-registry bundle Dockerfile/metadata generator.
257258
func (c bundleCmd) generateMetadata(manifestsDir, outputDir string) error {
259+
260+
metadataExists := checkMetatdataExists(outputDir, manifestsDir)
258261
err := bundle.GenerateFunc(manifestsDir, outputDir, c.operatorName, c.channels, c.defaultChannel, c.overwrite)
259262
if err != nil {
260263
return fmt.Errorf("error generating bundle metadata: %v", err)
261264
}
265+
266+
// Add SDK stamps if metadata is not present before or when overwrite is set to true.
267+
if c.overwrite || !metadataExists {
268+
rootDir := outputDir
269+
if rootDir == "" {
270+
rootDir = filepath.Dir(manifestsDir)
271+
}
272+
273+
if err = genbundle.RewriteBundleImageContents(rootDir); err != nil {
274+
return err
275+
}
276+
}
262277
return nil
263278
}
279+
280+
// checkMetatdataExists returns true if bundle.Dockerfile and metadataDir exist, if not
281+
// it returns false.
282+
func checkMetatdataExists(outputDir, manifestsDir string) bool {
283+
var annotationsDir string
284+
if outputDir == "" {
285+
annotationsDir = filepath.Dir(manifestsDir) + bundle.MetadataDir
286+
} else {
287+
annotationsDir = outputDir + bundle.MetadataDir
288+
}
289+
290+
if genutil.IsNotExist(bundle.DockerFile) || genutil.IsNotExist(annotationsDir) {
291+
return false
292+
}
293+
return true
294+
}

‎internal/generate/clusterserviceversion/clusterserviceversion.go

+20
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"strings"
2222

2323
"github.com/blang/semver"
24+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
2425
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
2526
"github.com/operator-framework/operator-registry/pkg/lib/bundle"
2627
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -160,13 +161,29 @@ func (g *Generator) Generate(cfg *config.Config, opts ...Option) (err error) {
160161
return err
161162
}
162163

164+
// Add sdk labels to csv
165+
setSDKAnnotations(csv)
166+
163167
w, err := g.getWriter()
164168
if err != nil {
165169
return err
166170
}
167171
return genutil.WriteObject(w, csv)
168172
}
169173

174+
// setSDKAnnotations adds SDK metric labels to the base if they do not exist.
175+
func setSDKAnnotations(csv *v1alpha1.ClusterServiceVersion) {
176+
annotations := csv.GetAnnotations()
177+
if annotations == nil {
178+
annotations = make(map[string]string)
179+
}
180+
181+
for key, value := range projutil.MakeOperatorMetricLabels() {
182+
annotations[key] = value
183+
}
184+
csv.SetAnnotations(annotations)
185+
}
186+
170187
// LegacyOption is a function that modifies a Generator for legacy project layouts.
171188
type LegacyOption Option
172189

@@ -204,6 +221,9 @@ func (g *Generator) GenerateLegacy(opts ...LegacyOption) (err error) {
204221
return err
205222
}
206223

224+
// Add sdk labels to csv
225+
setSDKAnnotations(csv)
226+
207227
w, err := g.getWriter()
208228
if err != nil {
209229
return err

‎internal/generate/clusterserviceversion/clusterserviceversion_test.go

+42-8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"io/ioutil"
2020
"os"
2121
"path/filepath"
22+
"strings"
2223

2324
. "github.com/onsi/ginkgo"
2425
. "github.com/onsi/gomega"
@@ -59,6 +60,11 @@ var (
5960
cfg *config.Config
6061
)
6162

63+
const (
64+
testSDKbuilderStamp = "operators.operatorframework.io/builder: operator-sdk-unknown"
65+
testSDKlayoutStamp = "operators.operatorframework.io/project_layout: unknown"
66+
)
67+
6268
var (
6369
baseCSV, baseCSVUIMeta, newCSV *v1alpha1.ClusterServiceVersion
6470
baseCSVStr, baseCSVUIMetaStr, newCSVStr string
@@ -121,7 +127,8 @@ var _ = Describe("Generating a ClusterServiceVersion", func() {
121127
WithWriter(buf),
122128
}
123129
Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred())
124-
Expect(buf.String()).To(MatchYAML(newCSVStr))
130+
outputCSV := removeSDKLabelsFromCSVString(buf.String())
131+
Expect(outputCSV).To(MatchYAML(newCSVStr))
125132
})
126133
It("should write a ClusterServiceVersion manifest to a base file", func() {
127134
g = Generator{
@@ -135,7 +142,27 @@ var _ = Describe("Generating a ClusterServiceVersion", func() {
135142
Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred())
136143
outputFile := filepath.Join(tmp, "bases", makeCSVFileName(operatorName))
137144
Expect(outputFile).To(BeAnExistingFile())
138-
Expect(string(readFileHelper(outputFile))).To(MatchYAML(baseCSVUIMetaStr))
145+
Expect(readFileHelper(outputFile)).To(MatchYAML(baseCSVUIMetaStr))
146+
})
147+
It("should have sdk labels in annotations", func() {
148+
g = Generator{
149+
OperatorName: operatorName,
150+
OperatorType: operatorType,
151+
}
152+
opts := []Option{
153+
WithBase(csvBasesDir, goAPIsDir, projutil.InteractiveHardOff),
154+
WithBaseWriter(tmp),
155+
}
156+
Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred())
157+
outputFile := filepath.Join(tmp, "bases", makeCSVFileName(operatorName))
158+
outputCSV, _, err := getCSVFromFile(outputFile)
159+
Expect(err).ToNot(HaveOccurred())
160+
Expect(outputFile).To(BeAnExistingFile())
161+
162+
annotations := outputCSV.GetAnnotations()
163+
Expect(annotations).ToNot(BeNil())
164+
Expect(annotations).Should(HaveKey(projutil.OperatorBuilder))
165+
Expect(annotations).Should(HaveKey(projutil.OperatorLayout))
139166
})
140167
It("should write a ClusterServiceVersion manifest to a bundle file", func() {
141168
g = Generator{
@@ -151,7 +178,7 @@ var _ = Describe("Generating a ClusterServiceVersion", func() {
151178
Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred())
152179
outputFile := filepath.Join(tmp, bundle.ManifestsDir, makeCSVFileName(operatorName))
153180
Expect(outputFile).To(BeAnExistingFile())
154-
Expect(string(readFileHelper(outputFile))).To(MatchYAML(newCSVStr))
181+
Expect(readFileHelper(outputFile)).To(MatchYAML(newCSVStr))
155182
})
156183
It("should write a ClusterServiceVersion manifest to a package file", func() {
157184
g = Generator{
@@ -167,7 +194,7 @@ var _ = Describe("Generating a ClusterServiceVersion", func() {
167194
Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred())
168195
outputFile := filepath.Join(tmp, g.Version, makeCSVFileName(operatorName))
169196
Expect(outputFile).To(BeAnExistingFile())
170-
Expect(string(readFileHelper(outputFile))).To(MatchYAML(newCSVStr))
197+
Expect(readFileHelper(outputFile)).To(MatchYAML(newCSVStr))
171198
})
172199

173200
It("should write a ClusterServiceVersion manifest to a legacy bundle file", func() {
@@ -184,7 +211,7 @@ var _ = Describe("Generating a ClusterServiceVersion", func() {
184211
Expect(g.GenerateLegacy(opts...)).ToNot(HaveOccurred())
185212
outputFile := filepath.Join(tmp, bundle.ManifestsDir, makeCSVFileName(operatorName))
186213
Expect(outputFile).To(BeAnExistingFile())
187-
Expect(string(readFileHelper(outputFile))).To(MatchYAML(newCSVStr))
214+
Expect(readFileHelper(outputFile)).To(MatchYAML(newCSVStr))
188215
})
189216
It("should write a ClusterServiceVersion manifest as a legacy package file", func() {
190217
g = Generator{
@@ -332,7 +359,6 @@ var _ = Describe("Generating a ClusterServiceVersion", func() {
332359
Expect(csv).To(Equal(upgradeCSV(newCSV, g.OperatorName, g.Version)))
333360
})
334361
})
335-
336362
})
337363

338364
})
@@ -397,10 +423,10 @@ func initTestCSVsHelper() {
397423
ExpectWithOffset(1, err).ToNot(HaveOccurred())
398424
}
399425

400-
func readFileHelper(path string) []byte {
426+
func readFileHelper(path string) string {
401427
b, err := ioutil.ReadFile(path)
402428
ExpectWithOffset(1, err).ToNot(HaveOccurred())
403-
return b
429+
return removeSDKLabelsFromCSVString(string(b))
404430
}
405431

406432
func modifyCSVDepImageHelper(tag string) func(csv *v1alpha1.ClusterServiceVersion) {
@@ -469,3 +495,11 @@ func upgradeCSV(csv *v1alpha1.ClusterServiceVersion, name, version string) *v1al
469495

470496
return upgraded
471497
}
498+
499+
// removeSDKLabelsFromCSVString to remove the sdk labels from test CSV structs. The test
500+
// cases do not generate a PROJECTFILE or an entire operator to get the version or layout
501+
// of SDK. Hence the values of those will appear "unknown".
502+
func removeSDKLabelsFromCSVString(csv string) string {
503+
replacer := strings.NewReplacer(testSDKbuilderStamp, "", testSDKlayoutStamp, "")
504+
return replacer.Replace(csv)
505+
}

‎internal/registry/validate.go

+60
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@ package registry
1616

1717
import (
1818
"fmt"
19+
"io/ioutil"
20+
"os"
1921

2022
apimanifests "github.com/operator-framework/api/pkg/manifests"
2123
apivalidation "github.com/operator-framework/api/pkg/validation"
2224
apierrors "github.com/operator-framework/api/pkg/validation/errors"
2325
registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle"
2426
log "github.com/sirupsen/logrus"
27+
"gopkg.in/yaml.v2"
2528
k8svalidation "k8s.io/apimachinery/pkg/api/validation"
2629
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
2731
"k8s.io/apimachinery/pkg/util/validation/field"
2832
)
2933

@@ -127,3 +131,59 @@ func appendResult(results []apierrors.ManifestResult, r apierrors.ManifestResult
127131

128132
return results
129133
}
134+
135+
// RewriteAnnotationsYaml unmarshalls the specified yaml file, appends the content and
136+
// converts it again to yaml.
137+
func RewriteAnnotationsYaml(filename string, content map[string]string) error {
138+
139+
metadata, err := getAnnotationFileContents(filename)
140+
if err != nil {
141+
return err
142+
}
143+
144+
// Append the contents to annotationsYaml
145+
for key, val := range content {
146+
metadata.Annotations[key] = val
147+
}
148+
149+
err = writeAnnotationFile(filename, metadata)
150+
if err != nil {
151+
return err
152+
}
153+
154+
return nil
155+
}
156+
157+
func getAnnotationFileContents(filename string) (*registrybundle.AnnotationMetadata, error) {
158+
f, err := ioutil.ReadFile(filename)
159+
if err != nil {
160+
return nil, err
161+
}
162+
163+
annotationsYaml := &registrybundle.AnnotationMetadata{}
164+
if err := yaml.Unmarshal(f, annotationsYaml); err != nil {
165+
return nil, fmt.Errorf("error parsing annotations file: %v", err)
166+
}
167+
return annotationsYaml, nil
168+
}
169+
170+
func writeAnnotationFile(filename string, annotation *registrybundle.AnnotationMetadata) error {
171+
// TODO: replace `gopkg.in/yaml.v2` with `sigs.k8s.io/yaml`. Operator registry
172+
// defines annotations with yaml format (using gopkg-yaml) and k8s-yaml takes name
173+
// of field in the struct as the value of key in yaml.
174+
file, err := yaml.Marshal(annotation)
175+
if err != nil {
176+
return err
177+
}
178+
179+
mode := os.FileMode(0666)
180+
if info, err := os.Stat(filename); err == nil {
181+
mode = info.Mode()
182+
}
183+
184+
err = ioutil.WriteFile(filename, []byte(file), mode)
185+
if err != nil {
186+
return fmt.Errorf("error writing modified contents to annotations file, %v", err)
187+
}
188+
return nil
189+
}
+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2020 The Operator-SDK Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package projutil
16+
17+
import (
18+
"regexp"
19+
20+
kbutil "github.com/operator-framework/operator-sdk/internal/util/kubebuilder"
21+
ver "github.com/operator-framework/operator-sdk/version"
22+
log "github.com/sirupsen/logrus"
23+
)
24+
25+
const (
26+
OperatorBuilder = "operators.operatorframework.io/builder"
27+
OperatorLayout = "operators.operatorframework.io/project_layout"
28+
bundleMediaType = "operators.operatorframework.io.metrics.mediatype.v1"
29+
bundleBuilder = "operators.operatorframework.io.metrics.builder"
30+
bundleLayout = "operators.operatorframework.io.metrics.project_layout"
31+
metricsMediatype = "metrics+v1"
32+
)
33+
34+
// MakeBundleMetricsLabels returns the SDK metric labels which will be added
35+
// to bundle resources like bundle.Dockerfile and annotations.yaml.
36+
func MakeBundleMetricsLabels() map[string]string {
37+
return map[string]string{
38+
bundleMediaType: metricsMediatype,
39+
bundleBuilder: getSDKBuilder(),
40+
bundleLayout: getSDKProjectLayout(),
41+
}
42+
}
43+
44+
// MakeOperatorMetricLabels returns the SDK metric labels which will be added
45+
// to custom resource definitions and cluster service versions.
46+
func MakeOperatorMetricLabels() map[string]string {
47+
return map[string]string{
48+
OperatorBuilder: getSDKBuilder(),
49+
OperatorLayout: getSDKProjectLayout(),
50+
}
51+
}
52+
53+
func getSDKBuilder() string {
54+
return "operator-sdk" + "-" + parseVersion(ver.GitVersion)
55+
}
56+
57+
func parseVersion(input string) string {
58+
re := regexp.MustCompile(`v[0-9]+\.[0-9]+\.[0-9]+`)
59+
version := re.FindString(input)
60+
if version == "" {
61+
return "unknown"
62+
}
63+
64+
if checkIfUnreleased(input) {
65+
version = version + "+git"
66+
}
67+
return version
68+
}
69+
70+
// checkIfUnreleased returns true if sdk was not built from released version.
71+
func checkIfUnreleased(input string) bool {
72+
re := regexp.MustCompile(`v[0-9]+\.[0-9]+\.[0-9]+-.+`)
73+
return re.MatchString(input)
74+
}
75+
76+
// getSDKProjectLayout returns the `layout` field in PROJECT file if it is a
77+
// Kubebuilder scaffolded project, or else returns the kind of operator.
78+
func getSDKProjectLayout() string {
79+
if kbutil.HasProjectFile() {
80+
cfg, err := kbutil.ReadConfig()
81+
if err != nil {
82+
log.Debugf("Error reading config: %v", err)
83+
return "unknown"
84+
}
85+
return cfg.Layout
86+
}
87+
return GetOperatorType()
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2020 The Operator-SDK Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package projutil
16+
17+
import (
18+
. "github.com/onsi/ginkgo"
19+
. "github.com/onsi/gomega"
20+
)
21+
22+
var _ = Describe("SDK Label helper functions", func() {
23+
Describe("parseVersion", func() {
24+
It("should extract sdk version", func() {
25+
version := "v0.17.0-159-ge87627f4-dirty"
26+
output := parseVersion(version)
27+
Expect(output).To(Equal("v0.17.0+git"))
28+
})
29+
It("should extract sdk version", func() {
30+
version := "v0.18.0"
31+
output := parseVersion(version)
32+
Expect(output).To(Equal("v0.18.0"))
33+
})
34+
It("should extract sdk version", func() {
35+
version := "v0.18.0-ge87627f4"
36+
output := parseVersion(version)
37+
Expect(output).To(Equal("v0.18.0+git"))
38+
})
39+
40+
})
41+
})

0 commit comments

Comments
 (0)
Please sign in to comment.