Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

scorecard: add bundle metadata #3474

Merged
merged 1 commit into from
Jul 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we please use sigs.k8s.io/yaml instead of this one for we still closer as possible to rip all these other modules to dealing with yaml as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That isn't possible until we bump operator-registry versions since the struct being marshalled only has yaml tags. I don't think we'll bump versions until after v1.0.

"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)
estroz marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
}