diff --git a/changelog/fragments/scorecard-bundle-metadata.yaml b/changelog/fragments/scorecard-bundle-metadata.yaml new file mode 100644 index 0000000000..01a525470c --- /dev/null +++ b/changelog/fragments/scorecard-bundle-metadata.yaml @@ -0,0 +1,6 @@ +entries: + - description: > + `generate bundle` now adds scorecard bundle metadata to bundle.Dockerfile and annotations.yaml + if `--overwrite` is set (the default in a project's `Makefile`) or both files do not exist. + kind: addition + breaking: false diff --git a/cmd/operator-sdk/generate/bundle/bundle.go b/cmd/operator-sdk/generate/bundle/bundle.go index e2360feecc..db6639439a 100644 --- a/cmd/operator-sdk/generate/bundle/bundle.go +++ b/cmd/operator-sdk/generate/bundle/bundle.go @@ -17,14 +17,19 @@ package bundle import ( "errors" "fmt" + "io/ioutil" "os" "path/filepath" + "sort" "strings" "github.com/operator-framework/operator-registry/pkg/lib/bundle" + yaml "gopkg.in/yaml.v3" "sigs.k8s.io/kubebuilder/pkg/model/config" genutil "github.com/operator-framework/operator-sdk/cmd/operator-sdk/generate/internal" + metricsannotations "github.com/operator-framework/operator-sdk/internal/annotations/metrics" + scorecardannotations "github.com/operator-framework/operator-sdk/internal/annotations/scorecard" gencsv "github.com/operator-framework/operator-sdk/internal/generate/clusterserviceversion" "github.com/operator-framework/operator-sdk/internal/generate/collector" "github.com/operator-framework/operator-sdk/internal/registry" @@ -231,7 +236,7 @@ func (c bundleCmd) validateMetadata(*config.Config) (err error) { } // runMetadata generates a bundle.Dockerfile and bundle metadata. -func (c bundleCmd) runMetadata() error { +func (c bundleCmd) runMetadata(cfg *config.Config) error { directory := c.inputDir if directory == "" { @@ -251,70 +256,74 @@ func (c bundleCmd) runMetadata() error { outputDir = "" } - return c.generateMetadata(directory, outputDir) + return c.generateMetadata(cfg, directory, outputDir) } // generateMetadata wraps the operator-registry bundle Dockerfile/metadata generator. -func (c bundleCmd) generateMetadata(manifestsDir, outputDir string) error { +func (c bundleCmd) generateMetadata(cfg *config.Config, manifestsDir, outputDir string) error { - metadataExists := checkMetatdataExists(outputDir, manifestsDir) + metadataExists := isMetatdataExist(outputDir, manifestsDir) err := bundle.GenerateFunc(manifestsDir, outputDir, c.operatorName, c.channels, c.defaultChannel, c.overwrite) if err != nil { return fmt.Errorf("error generating bundle metadata: %v", err) } - // Add SDK stamps if metadata is not present before or when overwrite is set to true. + // Add SDK annotations/labels if metadata did not exist before or when overwrite is true. if c.overwrite || !metadataExists { - rootDir := outputDir - if rootDir == "" { - rootDir = filepath.Dir(manifestsDir) + bundleRoot := outputDir + if bundleRoot == "" { + bundleRoot = filepath.Dir(manifestsDir) } - if err = rewriteBundleImageContents(rootDir); err != nil { + if err = updateMetadata(cfg, bundleRoot); err != nil { return err } } return nil } -func rewriteBundleImageContents(rootDir string) error { - metricLabels := projutil.MakeBundleMetricsLabels() - - // write metric labels to bundle.Dockerfile - if err := addLabelsToDockerfile(bundle.DockerFile, metricLabels); err != nil { - return fmt.Errorf("error writing metric labels to bundle.dockerfile: %v", err) +func updateMetadata(cfg *config.Config, bundleRoot string) error { + bundleLabels := metricsannotations.MakeBundleMetadataLabels(cfg) + for key, value := range scorecardannotations.MakeBundleMetadataLabels(scorecard.DefaultConfigDir) { + if _, hasKey := bundleLabels[key]; hasKey { + return fmt.Errorf("internal error: duplicate bundle annotation key %s", key) + } + bundleLabels[key] = value } - annotationsFilePath := getAnnotationsFilePath(rootDir) - if err := addLabelsToAnnotations(annotationsFilePath, metricLabels); err != nil { - return fmt.Errorf("error writing metric labels to annotations.yaml: %v", err) + // Write labels to bundle Dockerfile. + // NB(estroz): these "rewrites" need to be atomic because the bundle's Dockerfile and annotations.yaml + // cannot be out-of-sync. + if err := rewriteDockerfileLabels(bundle.DockerFile, bundleLabels); err != nil { + return fmt.Errorf("error writing LABEL's in %s: %v", bundle.DockerFile, err) + } + if err := rewriteAnnotations(bundleRoot, bundleLabels); err != nil { + return fmt.Errorf("error writing LABEL's in bundle metadata: %v", err) } - // Add a COPY for the scorecard config to bundle.Dockerfile. - if err := copyScorecardConfig(); err != nil { - return fmt.Errorf("error copying scorecardConfig to bundle image, %v", err) + // Add a COPY for the scorecard config to bundle Dockerfile. + // TODO: change input config path to be a flag-based value. + err := writeDockerfileCOPYScorecardConfig(bundle.DockerFile, filepath.FromSlash(scorecard.DefaultConfigDir)) + if err != nil { + return fmt.Errorf("error writing scorecard config COPY in %s: %v", bundle.DockerFile, err) } + return nil } -// copyScorecardConfigToBundle checks if bundle.Dockerfile and scorecard config exists in -// the operator project. If it does, it injects the scorecard configuration into bundle -// image. -// TODO: Add labels to annotations.yaml and bundle.dockerfile. -func copyScorecardConfig() error { - if isExist(bundle.DockerFile) && isExist(scorecard.ConfigDirName) { - scorecardFileContent := fmt.Sprintf("COPY %s %s\n", scorecard.ConfigDirName, scorecard.ConfigDirPath) - err := projutil.RewriteFileContents(bundle.DockerFile, "COPY", scorecardFileContent) - if err != nil { - return fmt.Errorf("error rewriting dockerfile, %v", err) - } +// writeDockerfileCOPYScorecardConfig checks if bundle.Dockerfile and scorecard config exists in +// the operator project. If it does, it injects the scorecard configuration into bundle image. +func writeDockerfileCOPYScorecardConfig(dockerfileName, localConfigDir string) error { + if isExist(bundle.DockerFile) && isExist(localConfigDir) { + scorecardFileContent := fmt.Sprintf("COPY %s %s\n", localConfigDir, "/"+scorecard.DefaultConfigDir) + return projutil.RewriteFileContents(dockerfileName, "COPY", scorecardFileContent) } return nil } -// checkMetatdataExists returns true if bundle.Dockerfile and metadataDir exist, if not +// isMetatdataExist returns true if bundle.Dockerfile and metadataDir exist, if not // it returns false. -func checkMetatdataExists(outputDir, manifestsDir string) bool { +func isMetatdataExist(outputDir, manifestsDir string) bool { var annotationsDir string if outputDir == "" { annotationsDir = filepath.Dir(manifestsDir) + bundle.MetadataDir @@ -328,30 +337,42 @@ func checkMetatdataExists(outputDir, manifestsDir string) bool { return true } -func addLabelsToDockerfile(filename string, metricAnnotation map[string]string) error { - var sdkMetricContent strings.Builder - for key, value := range metricAnnotation { - sdkMetricContent.WriteString(fmt.Sprintf("LABEL %s=%s\n", key, value)) +func rewriteDockerfileLabels(dockerfileName string, kvs map[string]string) error { + var labelStrings []string + for key, value := range kvs { + labelStrings = append(labelStrings, fmt.Sprintf("LABEL %s=%s\n", key, value)) } - - err := projutil.RewriteFileContents(filename, "LABEL", sdkMetricContent.String()) - if err != nil { - return fmt.Errorf("error rewriting dockerfile with metric labels, %v", err) + sort.Strings(labelStrings) + var newBundleLabels strings.Builder + for _, line := range labelStrings { + newBundleLabels.WriteString(line) } - return nil -} -// getAnnotationsFilePath return the locations of annotations.yaml. -func getAnnotationsFilePath(rootDir string) string { - return filepath.Join(rootDir, bundle.MetadataDir, bundle.AnnotationsFile) + return projutil.RewriteFileContents(dockerfileName, "LABEL", newBundleLabels.String()) } -func addLabelsToAnnotations(filename string, metricLables map[string]string) error { - err := registry.RewriteAnnotationsYaml(filename, metricLables) +func rewriteAnnotations(bundleRoot string, kvs map[string]string) error { + annotations, annotationsPath, err := registry.FindBundleMetadata(bundleRoot) if err != nil { return err } - return nil + + for key, value := range kvs { + annotations[key] = value + } + annotationsFile := bundle.AnnotationMetadata{ + Annotations: annotations, + } + b, err := yaml.Marshal(annotationsFile) + if err != nil { + return err + } + + mode := os.FileMode(0666) + if info, err := os.Stat(annotationsPath); err == nil { + mode = info.Mode() + } + return ioutil.WriteFile(annotationsPath, b, mode) } // isExist returns true if path exists. diff --git a/cmd/operator-sdk/generate/bundle/cmd.go b/cmd/operator-sdk/generate/bundle/cmd.go index 13719e9615..b34adf46d2 100644 --- a/cmd/operator-sdk/generate/bundle/cmd.go +++ b/cmd/operator-sdk/generate/bundle/cmd.go @@ -94,7 +94,7 @@ func NewCmd() *cobra.Command { } } if c.metadata { - if err = c.runMetadata(); err != nil { + if err = c.runMetadata(cfg); err != nil { log.Fatalf("Error generating bundle metadata: %v", err) } } diff --git a/cmd/operator-sdk/scorecard/cmd.go b/cmd/operator-sdk/scorecard/cmd.go index 25f68fcc3b..85105d76bc 100644 --- a/cmd/operator-sdk/scorecard/cmd.go +++ b/cmd/operator-sdk/scorecard/cmd.go @@ -29,6 +29,7 @@ import ( "github.com/spf13/viper" "k8s.io/apimachinery/pkg/labels" + scorecardannotations "github.com/operator-framework/operator-sdk/internal/annotations/scorecard" "github.com/operator-framework/operator-sdk/internal/flags" registryutil "github.com/operator-framework/operator-sdk/internal/registry" "github.com/operator-framework/operator-sdk/internal/scorecard" @@ -132,7 +133,11 @@ func (c *scorecardCmd) run() (err error) { configPath := c.config if configPath == "" { - configPath = filepath.Join(c.bundle, "tests", "scorecard", "config.yaml") + configDir, hasDir := scorecardannotations.GetConfigDir(metadata) + if !hasDir { + configDir = filepath.FromSlash(scorecard.DefaultConfigDir) + } + configPath = filepath.Join(c.bundle, configDir, scorecard.ConfigFileName) } o.Config, err = scorecard.LoadConfig(configPath) if err != nil { diff --git a/internal/annotations/metrics/metrics.go b/internal/annotations/metrics/metrics.go new file mode 100644 index 0000000000..cea41a68be --- /dev/null +++ b/internal/annotations/metrics/metrics.go @@ -0,0 +1,96 @@ +// Copyright 2020 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "regexp" + "strings" + + "sigs.k8s.io/kubebuilder/pkg/model/config" + + sdkversion "github.com/operator-framework/operator-sdk/version" +) + +// Static bundle annotation values. +const ( + mediaTypeV1 = "metrics+v1" +) + +// Bundle annotation keys. +const ( + mediaTypeBundleAnnotation = "operators.operatorframework.io.metrics.mediatype.v1" + builderBundleAnnotation = "operators.operatorframework.io.metrics.builder" + layoutBundleAnnotation = "operators.operatorframework.io.metrics.project_layout" +) + +// Object annotation keys. +const ( + BuilderObjectAnnotation = "operators.operatorframework.io/builder" + LayoutObjectAnnotation = "operators.operatorframework.io/project_layout" +) + +// MakeBundleMetadataLabels returns the SDK metric labels which will be added +// to bundle resources like bundle.Dockerfile and annotations.yaml. +func MakeBundleMetadataLabels(cfg *config.Config) map[string]string { + return map[string]string{ + mediaTypeBundleAnnotation: mediaTypeV1, + builderBundleAnnotation: getSDKBuilder(sdkversion.Version), + layoutBundleAnnotation: getSDKProjectLayout(cfg), + } +} + +// MakeObjectAnnotations returns the SDK metric annotations which will be added +// to CustomResourceDefinitions and ClusterServiceVersions. +func MakeBundleObjectAnnotations(cfg *config.Config) map[string]string { + return map[string]string{ + BuilderObjectAnnotation: getSDKBuilder(sdkversion.Version), + LayoutObjectAnnotation: getSDKProjectLayout(cfg), + } +} + +func getSDKBuilder(rawSDKVersion string) string { + return "operator-sdk" + "-" + parseVersion(rawSDKVersion) +} + +func parseVersion(input string) string { + re := regexp.MustCompile(`v[0-9]+\.[0-9]+\.[0-9]+`) + version := re.FindString(input) + if version == "" { + return "unknown" + } + + if isUnreleased(input) { + version = version + "+git" + } + return version +} + +// isUnreleased returns true if sdk was not built from released version. +func isUnreleased(input string) bool { + if strings.Contains(input, "+git") { + return true + } + re := regexp.MustCompile(`v[0-9]+\.[0-9]+\.[0-9]+-.+`) + return re.MatchString(input) +} + +// getSDKProjectLayout returns the `layout` field in PROJECT file that is v3. +// If not, it will return "go" because that was the only project type supported for project versions < v3. +func getSDKProjectLayout(cfg *config.Config) string { + if !cfg.IsV3() || cfg.Layout == "" { + return "go" + } + return cfg.Layout +} diff --git a/internal/util/projutil/sdk_stamps_util_test.go b/internal/annotations/metrics/metrics_test.go similarity index 98% rename from internal/util/projutil/sdk_stamps_util_test.go rename to internal/annotations/metrics/metrics_test.go index b5ff378e77..13c5537d45 100644 --- a/internal/util/projutil/sdk_stamps_util_test.go +++ b/internal/annotations/metrics/metrics_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package projutil +package metrics import ( . "github.com/onsi/ginkgo" diff --git a/internal/annotations/scorecard/scorecard.go b/internal/annotations/scorecard/scorecard.go new file mode 100644 index 0000000000..e1a6262cab --- /dev/null +++ b/internal/annotations/scorecard/scorecard.go @@ -0,0 +1,54 @@ +// Copyright 2020 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scorecard + +import ( + "path/filepath" +) + +// Static bundle annotation values. +const ( + mediaTypeV1 = "scorecard+v1" +) + +// Bundle annotation keys. +// NB(estroz): version these keys based on their "vX" version (either with the version in their names, +// or in subpackages). This may be a requirement if we create "v2" keys. +const ( + mediaTypeBundleKey = "operators.operatorframework.io.test.mediatype.v1" + configBundleKey = "operators.operatorframework.io.test.config.v1" +) + +func MakeBundleMetadataLabels(configDir string) map[string]string { + return map[string]string{ + mediaTypeBundleKey: mediaTypeV1, + configBundleKey: configDir, + } +} + +func GetConfigDir(labels map[string]string) (value string, hasKey bool) { + if configKey, hasMTKey := configKeyForMediaType(labels); hasMTKey { + value, hasKey = labels[configKey] + } + return filepath.Clean(filepath.FromSlash(value)), hasKey +} + +func configKeyForMediaType(labels map[string]string) (string, bool) { + switch labels[mediaTypeBundleKey] { + case mediaTypeV1: + return configBundleKey, true + } + return "", false +} diff --git a/internal/generate/clusterserviceversion/clusterserviceversion.go b/internal/generate/clusterserviceversion/clusterserviceversion.go index 675843df8a..5fb619ca6b 100644 --- a/internal/generate/clusterserviceversion/clusterserviceversion.go +++ b/internal/generate/clusterserviceversion/clusterserviceversion.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/kubebuilder/pkg/model/config" + metricsannotations "github.com/operator-framework/operator-sdk/internal/annotations/metrics" "github.com/operator-framework/operator-sdk/internal/generate/clusterserviceversion/bases" "github.com/operator-framework/operator-sdk/internal/generate/collector" genutil "github.com/operator-framework/operator-sdk/internal/generate/internal" @@ -162,7 +163,7 @@ func (g *Generator) Generate(cfg *config.Config, opts ...Option) (err error) { } // Add sdk labels to csv - setSDKAnnotations(csv) + g.setSDKAnnotations(csv) w, err := g.getWriter() if err != nil { @@ -172,13 +173,13 @@ func (g *Generator) Generate(cfg *config.Config, opts ...Option) (err error) { } // setSDKAnnotations adds SDK metric labels to the base if they do not exist. -func setSDKAnnotations(csv *v1alpha1.ClusterServiceVersion) { +func (g Generator) setSDKAnnotations(csv *v1alpha1.ClusterServiceVersion) { annotations := csv.GetAnnotations() if annotations == nil { annotations = make(map[string]string) } - for key, value := range projutil.MakeOperatorMetricLabels() { + for key, value := range metricsannotations.MakeBundleObjectAnnotations(g.config) { annotations[key] = value } csv.SetAnnotations(annotations) @@ -221,9 +222,6 @@ func (g *Generator) GenerateLegacy(opts ...LegacyOption) (err error) { return err } - // Add sdk labels to csv - setSDKAnnotations(csv) - w, err := g.getWriter() if err != nil { return err diff --git a/internal/generate/clusterserviceversion/clusterserviceversion_test.go b/internal/generate/clusterserviceversion/clusterserviceversion_test.go index e42c26b721..507580f177 100644 --- a/internal/generate/clusterserviceversion/clusterserviceversion_test.go +++ b/internal/generate/clusterserviceversion/clusterserviceversion_test.go @@ -16,10 +16,11 @@ package clusterserviceversion import ( "bytes" + "fmt" "io/ioutil" "os" "path/filepath" - "strings" + "regexp" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -33,6 +34,7 @@ import ( "sigs.k8s.io/kubebuilder/pkg/model/config" "sigs.k8s.io/yaml" + metricsannotations "github.com/operator-framework/operator-sdk/internal/annotations/metrics" "github.com/operator-framework/operator-sdk/internal/generate/collector" genutil "github.com/operator-framework/operator-sdk/internal/generate/internal" kbutil "github.com/operator-framework/operator-sdk/internal/util/kubebuilder" @@ -61,8 +63,8 @@ var ( ) const ( - testSDKbuilderStamp = "operators.operatorframework.io/builder: operator-sdk-unknown" - testSDKlayoutStamp = "operators.operatorframework.io/project_layout: unknown" + testSDKbuilderAnnotationKey = "operators.operatorframework.io/builder" + testSDKlayoutAnnotationKey = "operators.operatorframework.io/project_layout" ) var ( @@ -127,7 +129,7 @@ var _ = Describe("Generating a ClusterServiceVersion", func() { WithWriter(buf), } Expect(g.Generate(cfg, opts...)).ToNot(HaveOccurred()) - outputCSV := removeSDKLabelsFromCSVString(buf.String()) + outputCSV := removeSDKAnnotationsFromCSVString(buf.String()) Expect(outputCSV).To(MatchYAML(newCSVStr)) }) It("should write a ClusterServiceVersion manifest to a base file", func() { @@ -161,8 +163,8 @@ var _ = Describe("Generating a ClusterServiceVersion", func() { annotations := outputCSV.GetAnnotations() Expect(annotations).ToNot(BeNil()) - Expect(annotations).Should(HaveKey(projutil.OperatorBuilder)) - Expect(annotations).Should(HaveKey(projutil.OperatorLayout)) + Expect(annotations).Should(HaveKey(metricsannotations.BuilderObjectAnnotation)) + Expect(annotations).Should(HaveKey(metricsannotations.LayoutObjectAnnotation)) }) It("should write a ClusterServiceVersion manifest to a bundle file", func() { g = Generator{ @@ -426,7 +428,7 @@ func initTestCSVsHelper() { func readFileHelper(path string) string { b, err := ioutil.ReadFile(path) ExpectWithOffset(1, err).ToNot(HaveOccurred()) - return removeSDKLabelsFromCSVString(string(b)) + return removeSDKAnnotationsFromCSVString(string(b)) } func modifyCSVDepImageHelper(tag string) func(csv *v1alpha1.ClusterServiceVersion) { @@ -496,10 +498,14 @@ func upgradeCSV(csv *v1alpha1.ClusterServiceVersion, name, version string) *v1al return upgraded } -// removeSDKLabelsFromCSVString to remove the sdk labels from test CSV structs. The test -// cases do not generate a PROJECTFILE or an entire operator to get the version or layout -// of SDK. Hence the values of those will appear "unknown". -func removeSDKLabelsFromCSVString(csv string) string { - replacer := strings.NewReplacer(testSDKbuilderStamp, "", testSDKlayoutStamp, "") - return replacer.Replace(csv) +// removeSDKAnnotationsFromCSVString removes SDK annotations from test CSVs. +// These annotations will update on each new release and will cause tests to fail erroneously, +// so they should be removed for each test case. +func removeSDKAnnotationsFromCSVString(csv string) string { + builderRe := regexp.MustCompile(fmt.Sprintf(".*%s: .[^\n]+\n", regexp.QuoteMeta(testSDKbuilderAnnotationKey))) + layoutRe := regexp.MustCompile(fmt.Sprintf(".*%s: .[^\n]+\n", regexp.QuoteMeta(testSDKlayoutAnnotationKey))) + + csv = builderRe.ReplaceAllString(csv, "") + csv = layoutRe.ReplaceAllString(csv, "") + return csv } diff --git a/internal/registry/labels.go b/internal/registry/labels.go index 9b1ea95fc5..48c88c9d6a 100644 --- a/internal/registry/labels.go +++ b/internal/registry/labels.go @@ -36,15 +36,8 @@ type Labels map[string]string // GetManifestsDir returns the manifests directory name in ls using // a predefined key, or false if it does not exist. func (ls Labels) GetManifestsDir() (string, bool) { - value, hasLabel := ls.getLabel(registrybundle.ManifestsLabel) - return filepath.Clean(value), hasLabel -} - -// getLabel returns the string by key in ls, or an empty string and false -// if key is not found in ls. -func (ls Labels) getLabel(key string) (string, bool) { - value, hasLabel := ls[key] - return value, hasLabel + value, hasKey := ls[registrybundle.ManifestsLabel] + return filepath.Clean(value), hasKey } // FindBundleMetadata walks bundleRoot searching for metadata (ex. annotations.yaml), @@ -103,7 +96,7 @@ func readAnnotations(fs afero.Fs, annotationsPath string) (Labels, error) { // Use the arbitrarily-labelled bundle representation of the annotations file // for forwards and backwards compatibility. annotations := registrybundle.AnnotationMetadata{ - Annotations: make(map[string]string), + Annotations: make(Labels), } if err = yaml.Unmarshal(b, &annotations); err != nil { return nil, fmt.Errorf("error unmarshalling potential bundle metadata %s: %v", annotationsPath, err) diff --git a/internal/scorecard/config.go b/internal/scorecard/config.go index 5e4791dca7..9854b70abe 100644 --- a/internal/scorecard/config.go +++ b/internal/scorecard/config.go @@ -21,8 +21,10 @@ import ( ) const ( - ConfigDirName = "scorecard" - ConfigDirPath = "/tests/" + ConfigDirName + "/" + // ConfigFileName is the scorecard's hard-coded config file name. + ConfigFileName = "config.yaml" + // DefaultConfigDir is the default scorecard path within a bundle. + DefaultConfigDir = "tests/scorecard/" ) type Stage struct { diff --git a/internal/scorecard/examples/custom-scorecard-tests/bundle/metadata/annotations.yaml b/internal/scorecard/examples/custom-scorecard-tests/bundle/metadata/annotations.yaml index 4c60c3382b..9863e8e61a 100644 --- a/internal/scorecard/examples/custom-scorecard-tests/bundle/metadata/annotations.yaml +++ b/internal/scorecard/examples/custom-scorecard-tests/bundle/metadata/annotations.yaml @@ -1,7 +1,9 @@ annotations: - operators.operatorframework.io.bundle.mediatype.v1: "registry+v1" - operators.operatorframework.io.bundle.manifests.v1: "../manifests/" - operators.operatorframework.io.bundle.metadata.v1: "../metadata/" - operators.operatorframework.io.bundle.package.v1: "memcached-operator" - operators.operatorframework.io.bundle.channels.v1: "alpha,stable" - operators.operatorframework.io.bundle.channel.default.v1: "stable" + operators.operatorframework.io.bundle.mediatype.v1: registry+v1 + operators.operatorframework.io.bundle.manifests.v1: manifests/ + operators.operatorframework.io.bundle.metadata.v1: metadata/ + operators.operatorframework.io.bundle.package.v1: memcached-operator + operators.operatorframework.io.bundle.channels.v1: alpha,stable + operators.operatorframework.io.bundle.channel.default.v1: stable + operators.operatorframework.io.test.mediatype.v1: scorecard+v1 + operators.operatorframework.io.test.config.v1: tests/scorecard/ diff --git a/internal/scorecard/testdata/bundle.tar.gz b/internal/scorecard/testdata/bundle.tar.gz index 2b5cff95ac..e0b6c54589 100644 Binary files a/internal/scorecard/testdata/bundle.tar.gz and b/internal/scorecard/testdata/bundle.tar.gz differ diff --git a/internal/scorecard/testdata/bundle/metadata/annotations.yaml b/internal/scorecard/testdata/bundle/metadata/annotations.yaml index 1ccbc1bce5..9863e8e61a 100644 --- a/internal/scorecard/testdata/bundle/metadata/annotations.yaml +++ b/internal/scorecard/testdata/bundle/metadata/annotations.yaml @@ -5,3 +5,5 @@ annotations: operators.operatorframework.io.bundle.package.v1: memcached-operator operators.operatorframework.io.bundle.channels.v1: alpha,stable operators.operatorframework.io.bundle.channel.default.v1: stable + operators.operatorframework.io.test.mediatype.v1: scorecard+v1 + operators.operatorframework.io.test.config.v1: tests/scorecard/ diff --git a/internal/util/projutil/project_util.go b/internal/util/projutil/project_util.go index 1048d51e0d..0e87a51098 100644 --- a/internal/util/projutil/project_util.go +++ b/internal/util/projutil/project_util.go @@ -329,18 +329,15 @@ func PrintDeprecationWarning(msg string) { fmt.Fprintf(os.Stderr, noticeColor, "[Deprecation Notice] "+msg+"\n") } -// RewriteFileContents adds the provided content before the last occurrence of the word label -// and rewrites the file with the new content. -func RewriteFileContents(filename, instruction, content string) error { +// RewriteFileContents adds newContent to the line after the last occurrence of target in filename's contents, +// then writes the updated contents back to disk. +func RewriteFileContents(filename, target, newContent string) error { text, err := ioutil.ReadFile(filename) - if err != nil { return fmt.Errorf("error in getting contents from the file, %v", err) } - existingContent := string(text) - - modifiedContent, err := appendContent(existingContent, instruction, content) + modifiedContent, err := appendContent(string(text), target, newContent) if err != nil { return err } @@ -352,22 +349,18 @@ func RewriteFileContents(filename, instruction, content string) error { return nil } -func appendContent(fileContents, instruction, content string) (string, error) { - labelIndex := strings.LastIndex(fileContents, instruction) - +func appendContent(fileContents, target, newContent string) (string, error) { + labelIndex := strings.LastIndex(fileContents, target) if labelIndex == -1 { - return "", fmt.Errorf("instruction not present previously in dockerfile") + return "", fmt.Errorf("no prior string %s in newContent", target) } separationIndex := strings.Index(fileContents[labelIndex:], "\n") if separationIndex == -1 { - return "", fmt.Errorf("no new line at the end of dockerfile command %s", fileContents[labelIndex:]) + return "", fmt.Errorf("no new line at the end of string %s", fileContents[labelIndex:]) } index := labelIndex + separationIndex + 1 - - newContent := fileContents[:index] + content + fileContents[index:] - - return newContent, nil + return fileContents[:index] + newContent + fileContents[index:], nil } diff --git a/internal/util/projutil/projutil_test.go b/internal/util/projutil/projutil_test.go index 50d5bd9b94..636d4c3dd7 100644 --- a/internal/util/projutil/projutil_test.go +++ b/internal/util/projutil/projutil_test.go @@ -67,7 +67,7 @@ var _ = Describe("Testing projutil helpers", func() { _, err := appendContent(fileContents, instruction, content) - Expect(err).Should(MatchError(errors.New("instruction not present previously in dockerfile"))) + Expect(err).Should(MatchError(errors.New("no prior string ADD in newContent"))) }) It("Should result in error as no new line at the end of dockerfile command", func() { diff --git a/internal/util/projutil/sdk_stamps_util.go b/internal/util/projutil/sdk_stamps_util.go deleted file mode 100644 index 18d4d4643c..0000000000 --- a/internal/util/projutil/sdk_stamps_util.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package projutil - -import ( - "regexp" - - kbutil "github.com/operator-framework/operator-sdk/internal/util/kubebuilder" - ver "github.com/operator-framework/operator-sdk/version" - log "github.com/sirupsen/logrus" -) - -const ( - OperatorBuilder = "operators.operatorframework.io/builder" - OperatorLayout = "operators.operatorframework.io/project_layout" - bundleMediaType = "operators.operatorframework.io.metrics.mediatype.v1" - bundleBuilder = "operators.operatorframework.io.metrics.builder" - bundleLayout = "operators.operatorframework.io.metrics.project_layout" - metricsMediatype = "metrics+v1" -) - -// MakeBundleMetricsLabels returns the SDK metric labels which will be added -// to bundle resources like bundle.Dockerfile and annotations.yaml. -func MakeBundleMetricsLabels() map[string]string { - return map[string]string{ - bundleMediaType: metricsMediatype, - bundleBuilder: getSDKBuilder(), - bundleLayout: getSDKProjectLayout(), - } -} - -// MakeOperatorMetricLabels returns the SDK metric labels which will be added -// to custom resource definitions and cluster service versions. -func MakeOperatorMetricLabels() map[string]string { - return map[string]string{ - OperatorBuilder: getSDKBuilder(), - OperatorLayout: getSDKProjectLayout(), - } -} - -func getSDKBuilder() string { - return "operator-sdk" + "-" + parseVersion(ver.GitVersion) -} - -func parseVersion(input string) string { - re := regexp.MustCompile(`v[0-9]+\.[0-9]+\.[0-9]+`) - version := re.FindString(input) - if version == "" { - return "unknown" - } - - if checkIfUnreleased(input) { - version = version + "+git" - } - return version -} - -// checkIfUnreleased returns true if sdk was not built from released version. -func checkIfUnreleased(input string) bool { - re := regexp.MustCompile(`v[0-9]+\.[0-9]+\.[0-9]+-.+`) - return re.MatchString(input) -} - -// getSDKProjectLayout returns the `layout` field in PROJECT file if it is a -// Kubebuilder scaffolded project, or else returns the kind of operator. -func getSDKProjectLayout() string { - if kbutil.HasProjectFile() { - cfg, err := kbutil.ReadConfig() - if err != nil { - log.Debugf("Error reading config: %v", err) - return "unknown" - } - return cfg.Layout - } - return GetOperatorType() -} diff --git a/website/content/en/docs/advanced-topics/scorecard/scorecard.md b/website/content/en/docs/advanced-topics/scorecard/scorecard.md index 1a81d3b2f8..8b90af9908 100644 --- a/website/content/en/docs/advanced-topics/scorecard/scorecard.md +++ b/website/content/en/docs/advanced-topics/scorecard/scorecard.md @@ -30,26 +30,32 @@ require if the tests are designed for resource creation. ## Running the Scorecard -1. Define a scorecard configuration file `config.yaml`. A sample -configuration file [config.yaml][sample-config] is found within -the SDK repository. See the [config file section](#config-file) for an explaination -of the configuration file format. Unless you are executing custom -tests, you can just copy the provided example configuration file -into your project. -2. Place the scorecard configuration file within your project -bundle directory at the following location `tests/scorecard/config.yaml`. -You can override the default location of the configuration file -by specifying the `--config` flag. -3. Execute the [`scorecard` command][cli-scorecard]. See the -[command args section](#command-args) for an overview of command invocation. +1. Generate [bundle][quickstart-bundle] manifests and metadata for your Operator. +`make bundle` will automatically add scorecard annotations to your bundle's metadata, +which is used by the `scorecard` command to run tests. +1. Define a scorecard configuration file `bundle/tests/scorecard/config.yaml`, the default path. +If you choose to define this file elsewhere, you can either change the modified portion of that path in +`annotations.yaml` and `bundle.Dockerfile`, or override that value using the `--config` flag. +Unless executing custom tests, you may copy this [sample][sample-config] configuration file into your project. +See the [config file section](#config-file) for an explanation of the configuration file format. +1. Add the following line to the end of your `bundle.Dockerfile`: + ```docker + COPY bundle/tests/scorecard /tests/scorecard + ``` +1. Execute the [`scorecard` command][cli-scorecard]. See the [command args section](#command-args) +for an overview of command invocation. ## Configuration -The scorecard test execution is driven by a configuration file typically named -`config.yaml`. The configuration file is located at the following -location within your bundle: -``` -tests/scorecard/config.yaml +The scorecard test execution is driven by a configuration file named `config.yaml`. +The configuration file is located at the following location within your bundle directory (`bundle/` by default): +```sh +$ tree ./bundle +./bundle +... +└── tests + └── scorecard + └── config.yaml ``` ### Config File @@ -94,8 +100,8 @@ follows: ### Command Args The scorecard command has the following syntax: -``` -operator-sdk scorecard [flags] +```sh +$ operator-sdk scorecard [flags] ``` The scorecard requires a positional argument that holds either the @@ -254,6 +260,7 @@ Writing custom tests in other programming languages is possible if the test image follows the above guidelines. +[quickstart-bundle]: /docs/olm-integration/quickstart-bundle [cli-scorecard]: /docs/cli/operator-sdk_scorecard/ [sample-config]: https://github.com/operator-framework/operator-sdk/blob/master/internal/scorecard/testdata/bundle/tests/scorecard/config.yaml [custom-image]: https://github.com/operator-framework/operator-sdk/blob/master/internal/scorecard/examples/custom-scorecard-tests diff --git a/website/content/en/docs/olm-integration/quickstart-bundle.md b/website/content/en/docs/olm-integration/quickstart-bundle.md index 21ae2af50d..b84d6723e1 100644 --- a/website/content/en/docs/olm-integration/quickstart-bundle.md +++ b/website/content/en/docs/olm-integration/quickstart-bundle.md @@ -9,7 +9,7 @@ The Operator SDK supports both creating manifests for OLM deployment, and testin Kubernetes cluster. This document succinctly walks through getting an Operator OLM-ready with [bundles][bundle], and glosses over -explanations of certains steps for brevity. The following documents contain more detail on these steps: +explanations of certain steps for brevity. The following documents contain more detail on these steps: - All operator-framework manifest commands supported by the SDK: [CLI overview][doc-cli-overview]. - Generating operator-framework manifests: [generation overview][doc-olm-generate].