Skip to content

Commit

Permalink
scorecard: add bundle metadata (#3474)
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric Stroczynski committed Jul 21, 2020
1 parent 40aebd2 commit c51b80c
Show file tree
Hide file tree
Showing 19 changed files with 312 additions and 215 deletions.
6 changes: 6 additions & 0 deletions changelog/fragments/scorecard-bundle-metadata.yaml
Original file line number Diff line number Diff line change
@@ -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
121 changes: 71 additions & 50 deletions cmd/operator-sdk/generate/bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 == "" {
Expand All @@ -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
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion cmd/operator-sdk/generate/bundle/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
7 changes: 6 additions & 1 deletion cmd/operator-sdk/scorecard/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
96 changes: 96 additions & 0 deletions internal/annotations/metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
54 changes: 54 additions & 0 deletions internal/annotations/scorecard/scorecard.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit c51b80c

Please sign in to comment.